Skip to content

Commit

Permalink
Adjustments to the Serializable class
Browse files Browse the repository at this point in the history
 - Added a `transform` method for type conversions and more.
 - Include NBT and Serializable into the Sphinx documentation.
 - Added more explicit error messages to serializable tests.
 - Added the possibility to specify the error message/arguments in the tests.
 - Added pytest to the docs requirements to list the test function in the docs.
  • Loading branch information
LiteApplication committed May 21, 2024
1 parent 56a896f commit bbab587
Show file tree
Hide file tree
Showing 18 changed files with 414 additions and 359 deletions.
90 changes: 0 additions & 90 deletions changes/273.internal.md

This file was deleted.

106 changes: 106 additions & 0 deletions changes/285.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
- Changed the way `Serializable` classes are handled:

Here is how a basic `Serializable` class looks like:
```python
@final
@dataclass
class ToyClass(Serializable):
"""Toy class for testing demonstrating the use of gen_serializable_test on `Serializable`."""

a: int
b: str | int

@override
def serialize_to(self, buf: Buffer):
"""Write the object to a buffer."""
self.b = cast(str, self.b) # Handled by the transform method
buf.write_varint(self.a)
buf.write_utf(self.b)

@classmethod
@override
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)

@override
def validate(self) -> None:
"""Validate the object's attributes."""
if self.a == 0:
raise ZeroDivisionError("a must be non-zero")
if (isinstance(self.b, int) and math.log10(self.b) > 10) or (isinstance(self.b, str) and len(self.b) > 10):
raise ValueError("b must be less than 10 characters")

@override
def transform(self) -> None:
"""Apply a transformation to the payload of the object."""
if isinstance(self.b, int):
self.b = str(self.b)
```


The `Serializable` class 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.

And the following optional methods:
- `validate() -> None`: Validates the object's attributes, raising an exception if they are invalid.
- `transform() -> None`: Transforms the the object's attributes, this method is meant to convert types like you would in a classic `__init__`.
You can rely on this `validate` having been executed.

- 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 for `locals()`.
- `cls`: The `Serializable` class to generate tests for.
- `fields`: A list of fields where the test values will be placed.

> In the example above, the `ToyClass` class has two fields: `a` and `b`.
- `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:
```python
gen_serializable_test(
context=globals(),
cls=ToyClass,
fields=[("a", int), ("b", str)],
test_data=[
((1, "hello"), b"\x01\x05hello"),
((2, "world"), b"\x02\x05world"),
((3, 1234567890), b"\x03\x0a1234567890"),
((0, "hello"), ZeroDivisionError("a must be non-zero")), # With an error message
((1, "hello world"), ValueError), # No error message
((1, 12345678900), ValueError),
(ZeroDivisionError, b"\x00"),
(ZeroDivisionError, b"\x01\x05hello"),
(IOError, b"\x01"),
],
)
```

The generated test class will have the following tests:

```python
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
```
3 changes: 3 additions & 0 deletions docs/api/internal.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,8 @@ as an easy quick reference for contributors. These components **are not a part o
should not be used externally**, as we do not guarantee their backwards compatibility, which means breaking changes
may be introduced between patch versions without any warnings.

.. automodule:: mcproto.utils.abc

.. autofunction:: tests.helpers.gen_serializable_test
..
TODO: Write this
12 changes: 12 additions & 0 deletions docs/api/types/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.. api/types documentation master file
=======================
API Types Documentation
=======================

Welcome to the API Types documentation! This documentation provides information about the various types used in the API.

.. toctree::
:maxdepth: 2

nbt.rst
6 changes: 6 additions & 0 deletions docs/api/types/nbt.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
NBT Format
==========

.. automodule:: mcproto.types.nbt
:members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Content
api/packets.rst
api/protocol.rst
api/internal.rst
api/types/index.rst


Indices and tables
Expand Down
14 changes: 8 additions & 6 deletions mcproto/packets/handshaking/handshake.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class Handshake(ServerBoundPacket):
@override
def serialize_to(self, buf: Buffer) -> None:
"""Serialize the packet."""
self.next_state = cast(NextState, self.next_state) # Handled by the validate method
self.next_state = cast(NextState, self.next_state) # Handled by the transform method
buf.write_varint(self.protocol_version)
buf.write_utf(self.server_address)
buf.write_value(StructFormat.USHORT, self.server_port)
Expand All @@ -65,10 +65,12 @@ def _deserialize(cls, buf: Buffer, /) -> Self:

@override
def validate(self) -> None:
"""Validate the packet."""
if not isinstance(self.next_state, NextState):
rev_lookup = {x.value: x for x in NextState.__members__.values()}
try:
self.next_state = rev_lookup[self.next_state]
except KeyError as exc:
raise ValueError("No such next_state.") from exc
if self.next_state not in rev_lookup:
raise ValueError("No such next_state.")

@override
def transform(self) -> None:
"""Get the next state enum from the integer value."""
self.next_state = NextState(self.next_state)
5 changes: 2 additions & 3 deletions mcproto/packets/login/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ def _deserialize(cls, buf: Buffer, /) -> Self:
return cls(server_id=server_id, public_key=public_key, verify_token=verify_token)

@override
def validate(self) -> None:
"""Validate the packet."""
def transform(self) -> None:
if self.server_id is None:
self.server_id = " " * 20

Expand Down Expand Up @@ -266,7 +265,7 @@ class LoginSetCompression(ClientBoundPacket):
Maximum size of a packet before it is compressed. All packets smaller than this will remain uncompressed.
To disable compression completely, threshold can be set to -1.
:note: This packet is optional, and if not set, the compression will not be enabled at all.
.. note:: This packet is optional, and if not set, the compression will not be enabled at all.
"""

PACKET_ID: ClassVar[int] = 0x03
Expand Down
2 changes: 1 addition & 1 deletion mcproto/packets/status/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class StatusResponse(ClientBoundPacket):

@override
def serialize_to(self, buf: Buffer) -> None:
s = json.dumps(self.data)
s = json.dumps(self.data, separators=(",", ":"))
buf.write_utf(s)

@override
Expand Down
Loading

0 comments on commit bbab587

Please sign in to comment.