Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Serializables (v3) #285

Merged
merged 5 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions changes/285.internal.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
- **Function**: `gen_serializable_test`
- Generates tests for serializable classes, covering serialization, deserialization, validation, and error handling.
- **Parameters**:
- `context` (dict): Context to add the test functions to (usually `globals()`).
- `cls` (type): The serializable class to test.
- `fields` (list): Tuples of field names and types of the serializable class.
- `serialize_deserialize` (list, optional): Tuples for testing successful serialization/deserialization.
- `validation_fail` (list, optional): Tuples for testing validation failures with expected exceptions.
- `deserialization_fail` (list, optional): Tuples for testing deserialization failures with expected exceptions.
- **Note**: Implement `__eq__` in the class for accurate comparison.

- The `gen_serializable_test` function generates a test class with the following tests:

.. literalinclude:: /../tests/mcproto/utils/test_serializable.py
:language: python
:start-after: # region Test ToyClass
:end-before: # endregion Test ToyClass

- The generated test class will have the following tests:

```python
class TestGenToyClass:
def test_serialization(self):
# 3 subtests for the cases 1, 2, 3 (serialize_deserialize)

def test_deserialization(self):
# 3 subtests for the cases 1, 2, 3 (serialize_deserialize)

def test_validation(self):
# 3 subtests for the cases 4, 5, 6 (validation_fail)

def test_exceptions(self):
# 3 subtests for the cases 7, 8, 9 (deserialization_fail)
```
16 changes: 16 additions & 0 deletions changes/285.internal.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
- **Class**: `Serializable`
- Base class for types that should be (de)serializable into/from `mcproto.Buffer` data.
- **Methods**:
- `__attrs_post_init__()`: Runs validation after object initialization, override to define custom behavior.
- `serialize() -> Buffer`: Returns the object as a `Buffer`.
- `serialize_to(buf: Buffer)`: Abstract method to write the object to a `Buffer`.
- `validate()`: Validates the object's attributes; can be overridden for custom validation.
- `deserialize(cls, buf: Buffer) -> Self`: Abstract method to construct the object from a `Buffer`.
- **Note**: Use the `dataclass` decorator when adding parameters to subclasses.

- Exemple:

.. literalinclude:: /../tests/mcproto/utils/test_serializable.py
:language: python
:start-after: # region ToyClass
:end-before: # endregion ToyClass
4 changes: 4 additions & 0 deletions docs/api/internal.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@ 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
:exclude-members: define

.. 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
66 changes: 33 additions & 33 deletions mcproto/packets/handshaking/handshake.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from __future__ import annotations

from enum import IntEnum
from typing import ClassVar, final
from typing import ClassVar, cast, final

from typing_extensions import Self, override

from mcproto.buffer import Buffer
from mcproto.packets.packet import GameState, ServerBoundPacket
from mcproto.protocol.base_io import StructFormat
from attrs import define

__all__ = [
"Handshake",
"NextState",
"Handshake",
]


Expand All @@ -23,49 +24,41 @@ class NextState(IntEnum):


@final
@define
class Handshake(ServerBoundPacket):
"""Initializes connection between server and client. (Client -> Server)."""
"""Initializes connection between server and client. (Client -> Server).

Initialize the Handshake packet.

:param protocol_version: Protocol version number to be used.
:param server_address: The host/address the client is connecting to.
:param server_port: The port the client is connecting to.
:param next_state: The next state for the server to move into.
"""

PACKET_ID: ClassVar[int] = 0x00
GAME_STATE: ClassVar[GameState] = GameState.HANDSHAKING

__slots__ = ("next_state", "protocol_version", "server_address", "server_port")

def __init__(
self,
*,
protocol_version: int,
server_address: str,
server_port: int,
next_state: NextState | int,
):
"""Initialize the Handshake packet.

:param protocol_version: Protocol version number to be used.
:param server_address: The host/address the client is connecting to.
:param server_port: The port the client is connecting to.
:param next_state: The next state for the server to move into.
"""
if not isinstance(next_state, NextState): # next_state is int
rev_lookup = {x.value: x for x in NextState.__members__.values()}
try:
next_state = rev_lookup[next_state]
except KeyError as exc:
raise ValueError("No such next_state.") from exc
protocol_version: int
server_address: str
server_port: int
next_state: NextState | int

self.protocol_version = protocol_version
self.server_address = server_address
self.server_port = server_port
self.next_state = next_state
@override
def __attrs_post_init__(self) -> None:
if not isinstance(self.next_state, NextState):
self.next_state = NextState(self.next_state)

super().__attrs_post_init__()

@override
def serialize(self) -> Buffer:
buf = Buffer()
def serialize_to(self, buf: Buffer) -> None:
"""Serialize the packet."""
self.next_state = cast(NextState, self.next_state) # Handled by the __attrs_post_init__ method
buf.write_varint(self.protocol_version)
buf.write_utf(self.server_address)
buf.write_value(StructFormat.USHORT, self.server_port)
buf.write_varint(self.next_state.value)
return buf

@override
@classmethod
Expand All @@ -76,3 +69,10 @@ def _deserialize(cls, buf: Buffer, /) -> Self:
server_port=buf.read_value(StructFormat.USHORT),
next_state=buf.read_varint(),
)

@override
def validate(self) -> None:
if not isinstance(self.next_state, NextState):
rev_lookup = {x.value: x for x in NextState.__members__.values()}
if self.next_state not in rev_lookup:
raise ValueError("No such next_state.")
Loading
Loading