Skip to content

Commit

Permalink
WIP: Feature dataclass type hinting
Browse files Browse the repository at this point in the history
  • Loading branch information
qstokkink committed Oct 7, 2021
1 parent 5b88423 commit b92a8e8
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 29 deletions.
25 changes: 21 additions & 4 deletions doc/reference/serialization.rst
Expand Up @@ -17,12 +17,12 @@ You can use the ``Serializer`` with classes of the following types:
"Serializable", "ipv8/messaging/serialization.py", "Base class for all things serializable. Should support the instance method to_pack_list() and the class method from_unpack_list()."
"Payload", "ipv8/messaging/payload.py", "Extension of the Serializable class with logic for pretty printing."
"VariablePayload", "ipv8/messaging/lazy_payload.py", "Less verbose way to specify Payloads, at the cost of performance."
"dataclass_payload", "ipv8/messaging/payload_dataclass.py", "Use dataclasses to send messages, at the cost of control and performance."
"dataclass", "ipv8/messaging/payload_dataclass.py", "Use dataclasses to send messages, at the cost of control and performance."


Other than the ``dataclass_payload``, each of these serializable classes specifies a list of primitive data types it will serialize to and from.
Other than the ``dataclass``, each of these serializable classes specifies a list of primitive data types it will serialize to and from.
The primitive data types are specified in the :ref:`data types<Datatypes Section>` Section.
Each serializable class has to specify the following class members (``dataclass_payload`` does this automatically):
Each serializable class has to specify the following class members (``dataclass`` does this automatically):

.. csv-table:: Serializable class members
:header: "member", "description"
Expand All @@ -34,7 +34,7 @@ Each serializable class has to specify the following class members (``dataclass_

As an example, we will now define four completely wire-format compatible messages using the four classes.
Each of the messages will serialize to a (four byte) unsigned integer followed by an (two byte) unsigned short.
If the ``dataclass_payload`` had used normal ``int`` types, these would have been two signed 8-byte integers instead.
If the ``dataclass`` had used normal ``int`` types, these would have been two signed 8-byte integers instead.
Each instance will have two fields: ``field1`` and ``field2`` corresponding to the integer and short.

.. literalinclude:: serialization_1.py
Expand Down Expand Up @@ -65,6 +65,20 @@ To show some of the differences, let's check out the output of the following scr
| field1: 1
| field2: 2
Type hinting with the dataclass wrapper
---------------------------------------

For the ``@dataclass`` wrapper you need a special import construction to get type hinting:

.. code-block:: python
from dataclasses import dataclass
from ipv8.messaging.payload_dataclass import dataclass
This will fool your IDE into providing type hints.


.. _Datatypes Section:

Datatypes
Expand Down Expand Up @@ -162,4 +176,7 @@ It is recommended (but not obligatory) to have single payload messages store the
:lines: 31,32,53,56
:dedent: 4

If you are using the ``@dataclass`` wrapper you can specify the message identifier through an argument instead.
For example, ``@dataclass(msg_id=42)`` would set the message identifier to ``42``.

Of course, IPv8 also ships with various ``Community`` subclasses of its own, if you need inspiration.
14 changes: 7 additions & 7 deletions ipv8/messaging/payload_dataclass.py
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass as ogdataclass
from functools import partial
from typing import TypeVar, Type, get_type_hints

Expand All @@ -25,17 +25,16 @@ def type_map(t: Type) -> str:
raise NotImplementedError(t, " unknown")


def dataclass_payload(cls=None, /, *, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False,
msg_id=None):
def dataclass(cls=None, /, *, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False, msg_id=None):
"""
Equivalent to ``@dataclass``, but also makes the wrapped class a ``VariablePayload``.
See ``dataclasses.dataclass`` for argument descriptions.
"""
if cls is None:
return partial(dataclass_payload, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash,
return partial(dataclass, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash,
frozen=frozen, msg_id=msg_id)
origin = dataclass(cls, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen)
origin = ogdataclass(cls, init=init, repr=repr, eq=eq, order=order, unsafe_hash=unsafe_hash, frozen=frozen)

class DataClassPayload(origin, VariablePayload):
names = list(get_type_hints(origin).keys())
Expand All @@ -45,7 +44,8 @@ class DataClassPayload(origin, VariablePayload):
setattr(DataClassPayload, "msg_id", msg_id)
DataClassPayload.__name__ = origin.__name__
DataClassPayload.__qualname__ = origin.__qualname__
return vp_compile(DataClassPayload)
out: ogdataclass = vp_compile(DataClassPayload)
return out


__all__ = ['dataclass_payload']
__all__ = ['dataclass']
36 changes: 18 additions & 18 deletions ipv8/test/messaging/test_payload_dataclass.py
@@ -1,88 +1,88 @@
from dataclasses import dataclass, is_dataclass
from dataclasses import dataclass, dataclass as ogdataclass, is_dataclass
from typing import List, TypeVar

from ..base import TestBase
from ...messaging.payload_dataclass import dataclass_payload
from ...messaging.payload_dataclass import dataclass
from ...messaging.serialization import default_serializer


varlenH = TypeVar('varlenH')


@dataclass_payload
@dataclass
class NativeBool:
a: bool


@dataclass_payload
@dataclass
class NativeInt:
a: int


@dataclass_payload
@dataclass
class NativeBytes:
a: bytes


@dataclass_payload
@dataclass
class NativeStr:
a: str


@dataclass_payload
@dataclass
class SerializerType:
a: varlenH


@dataclass_payload
@dataclass
class NestedType:
a: NativeInt


@dataclass_payload
@dataclass
class NestedListType:
a: [NativeInt]


@dataclass_payload
@dataclass
class NestedListTypeType:
a: List[NativeInt]


@dataclass
@ogdataclass
class Unknown:
"""
To whomever is reading this and wondering why dict is not supported: use a nested payload instead.
"""
a: dict


@dataclass_payload
@dataclass
class A:
a: int
b: int


@dataclass_payload(eq=False)
@dataclass(eq=False)
class FwdDataclass:
a: int


@dataclass_payload
@dataclass
class StripMsgId:
a: int
msg_id = 1


@dataclass_payload(msg_id=1)
@dataclass(msg_id=1)
class FwdMsgId:
a: int


@dataclass_payload
@dataclass
@ogdataclass
class Everything:
@dataclass_payload
@dataclass
class Item:
a: bool

Expand Down Expand Up @@ -294,7 +294,7 @@ def test_unknown_payload(self):
"""
Check if an unknown type raises an error.
"""
self.assertRaises(NotImplementedError, dataclass_payload, Unknown)
self.assertRaises(NotImplementedError, dataclass, Unknown)

def test_nestedlisttype_filled_payload(self):
"""
Expand Down

0 comments on commit b92a8e8

Please sign in to comment.