New Serializables (v3)
This uses the Serializable (#273) methods to validate and use the data in the NBTags (#257) subclasses.
The tests also make use of the new test generator introduced in #273.
EDIT : Let's merge this with 273 !
Original PR description :
Changed the way Serializable classes are handled:
Here is how a basic Serializable class looks like:
@final
@dataclass
class ToyClass(Serializable):
"""Toy class for testing demonstrating the use of gen_serializable_test on `Serializable`."""
# Attributes can be of any type
a: int
b: str
# dataclasses.field() can be used to specify additional metadata
def serialize_to(self, buf: Buffer):
"""Write the object to a buffer."""
buf.write_varint(self.a)
buf.write_utf(self.b)
@classmethod
def deserialize(cls, buf: Buffer) -> ToyClass:
"""Deserialize the object from a buffer."""
a = buf.read_varint()
if a == 0:
raise ZeroDivisionError("a must be non-zero")
b = buf.read_utf()
return cls(a, b)
def validate(self) -> None:
"""Validate the object's attributes."""
if self.a == 0:
raise ZeroDivisionError("a must be non-zero")
if len(self.b) > 10:
raise ValueError("b must be less than 10 characters")
The Serializable class must implement the following methods:
-
serialize_to(buf: Buffer) -> None: Serializes the object to a buffer. -
deserialize(buf: Buffer) -> Serializable: Deserializes the object from a buffer. -
validate() -> None: Validates the object's attributes, raising an exception if they are invalid.
Added a test generator for Serializable classes:
The gen_serializable_test function generates tests for Serializable classes. It takes the following arguments:
-
context: The dictionary containing the context in which the generated test class will be placed (e.g.globals()).Dictionary updates must reflect in the context. This is the case for
globals()but implementation-specific forlocals(). -
cls: TheSerializableclass to generate tests for. -
fields: A list of fields where the test values will be placed.In the example above, the
ToyClassclass has two fields:aandb. -
test_data: A list of tuples containing either:-
((field1_value, field2_value, ...), expected_bytes): The values of the fields and the expected serialized bytes. This needs to work both ways, i.e.cls(field1_value, field2_value, ...) == cls.deserialize(expected_bytes). -
((field1_value, field2_value, ...), exception): The values of the fields and the expected exception when validating the object. -
(exception, bytes): The expected exception when deserializing the bytes and the bytes to deserialize.
-
The gen_serializable_test function generates a test class with the following tests:
gen_serializable_test(
context=globals(),
cls=ToyClass,
fields=[("a", int), ("b", str)],
test_data=[
((1, "hello"), b"\x01\x05hello"),
((2, "world"), b"\x02\x05world"),
((0, "hello"), ZeroDivisionError),
((1, "hello world"), ValueError),
(ZeroDivisionError, b"\x00"),
(IOError, b"\x01"),
],
)
The generated test class will have the following tests:
class TestGenToyClass:
def test_serialization(self):
# 2 subtests for the cases 1 and 2
def test_deserialization(self):
# 2 subtests for the cases 1 and 2
def test_validation(self):
# 2 subtests for the cases 3 and 4
def test_exceptions(self):
# 2 subtests for the cases 5 and 6
Also this has 100% test coverage for all data types and packets.
Now that NBTag support was merged, this can become a part of #273, or alternatively, this can replace #273.
I'm currently merging the changes added right after #257. I'm OK with replacing #273 with this PR