diff --git a/changes/273.internal.md b/changes/273.internal.md deleted file mode 100644 index 4096e20f..00000000 --- a/changes/273.internal.md +++ /dev/null @@ -1,90 +0,0 @@ -- 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 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: - - 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 diff --git a/changes/285.internal.md b/changes/285.internal.md new file mode 100644 index 00000000..f077b376 --- /dev/null +++ b/changes/285.internal.md @@ -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 +``` diff --git a/docs/api/internal.rst b/docs/api/internal.rst index d9272a6d..9bd051d6 100644 --- a/docs/api/internal.rst +++ b/docs/api/internal.rst @@ -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 diff --git a/docs/api/types/index.rst b/docs/api/types/index.rst new file mode 100644 index 00000000..f17972a8 --- /dev/null +++ b/docs/api/types/index.rst @@ -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 diff --git a/docs/api/types/nbt.rst b/docs/api/types/nbt.rst new file mode 100644 index 00000000..e2a4398b --- /dev/null +++ b/docs/api/types/nbt.rst @@ -0,0 +1,6 @@ +NBT Format +========== + +.. automodule:: mcproto.types.nbt + :members: + :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst index d325b281..18170ab2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -24,6 +24,7 @@ Content api/packets.rst api/protocol.rst api/internal.rst + api/types/index.rst Indices and tables diff --git a/mcproto/packets/handshaking/handshake.py b/mcproto/packets/handshaking/handshake.py index 72435f40..74182c59 100644 --- a/mcproto/packets/handshaking/handshake.py +++ b/mcproto/packets/handshaking/handshake.py @@ -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) @@ -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) diff --git a/mcproto/packets/login/login.py b/mcproto/packets/login/login.py index bffe3eb7..f5708f03 100644 --- a/mcproto/packets/login/login.py +++ b/mcproto/packets/login/login.py @@ -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 @@ -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 diff --git a/mcproto/packets/status/status.py b/mcproto/packets/status/status.py index f9a5884c..25d4c349 100644 --- a/mcproto/packets/status/status.py +++ b/mcproto/packets/status/status.py @@ -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 diff --git a/mcproto/types/nbt.py b/mcproto/types/nbt.py index 4fc0a04b..381838cf 100644 --- a/mcproto/types/nbt.py +++ b/mcproto/types/nbt.py @@ -5,11 +5,12 @@ from typing import ClassVar, Union, cast, Protocol, final, runtime_checkable from collections.abc import Iterator, Mapping, Sequence -from typing_extensions import TypeAlias, override +from typing_extensions import TypeAlias, override, Self from mcproto.buffer import Buffer from mcproto.protocol.base_io import StructFormat, INT_FORMATS_TYPE, FLOAT_FORMATS_TYPE from mcproto.types.abc import MCType, dataclass +from mcproto.utils.abc import RequiredParamsABCMixin __all__ = [ "ByteArrayNBT", @@ -404,9 +405,8 @@ def from_object(data: FromObjectType, schema: FromObjectSchema, name: str = "") if isinstance(first_schema, (list, dict)) and not all(isinstance(item, type(first_schema)) for item in schema): raise TypeError(f"Expected a list of lists or dictionaries, but found a different type ({schema=}).") # NBTag case - # Now don't get me wrong, this is actually covered but the coverage tool thinks that it's missing a case with - # an empty list, which is not possible because of the previous checks - if isinstance(first_schema, type) and not all(item == first_schema for item in schema): # pragma: no cover + # Ignore branch coverage, `schema` will never be an empty list here + if isinstance(first_schema, type) and not all(item == first_schema for item in schema): # pragma: no branch raise TypeError(f"The schema must contain a single type of elements. ({schema=})") for item, sub_schema in zip(data, schema): @@ -447,15 +447,6 @@ def __repr__(self) -> str: return f"{type(self).__name__}[{self.name!r}]({self.payload!r})" return f"{type(self).__name__}({self.payload!r})" - @override - def __eq__(self, other: object) -> bool: - """Check equality between two NBT tags.""" - if not isinstance(other, NBTag): - return NotImplemented - if type(self) is not type(other): - return False - return self.name == other.name and self.payload == other.payload - @override def to_nbt(self, name: str = "") -> NBTag: """Convert the object to an NBT tag. @@ -509,12 +500,14 @@ def value(self) -> PayloadType: @dataclass -class _NumberNBTag(NBTag): +class _NumberNBTag(NBTag, RequiredParamsABCMixin): """Base class for NBT tags representing a number. This class is not meant to be used directly, but rather through its subclasses. """ + _REQUIRED_CLASS_VARS = ("STRUCT_FORMAT", "DATA_SIZE") + STRUCT_FORMAT: ClassVar[INT_FORMATS_TYPE] = NotImplemented # type: ignore DATA_SIZE: ClassVar[int] = NotImplemented @@ -528,7 +521,7 @@ def serialize_to(self, buf: Buffer, with_type: bool = True, with_name: bool = Tr @override @classmethod - def read_from(cls, buf: Buffer, with_type: bool = True, with_name: bool = True) -> _NumberNBTag: + def read_from(cls, buf: Buffer, with_type: bool = True, with_name: bool = True) -> Self: name, tag_type = cls._read_header(buf, read_type=with_type, with_name=with_name) if _get_tag_type(cls) != tag_type: raise TypeError(f"Expected a {_get_tag_type(cls).name} tag, but found a different tag ({tag_type.name}).") @@ -593,9 +586,11 @@ class LongNBT(_NumberNBTag): @dataclass -class _FloatingNBTag(NBTag): +class _FloatingNBTag(NBTag, RequiredParamsABCMixin): """Base class for NBT tags representing a floating-point number.""" + _REQUIRED_CLASS_VARS = ("STRUCT_FORMAT", "DATA_SIZE") + STRUCT_FORMAT: ClassVar[FLOAT_FORMATS_TYPE] = NotImplemented # type: ignore DATA_SIZE: ClassVar[int] = NotImplemented @@ -609,7 +604,7 @@ def serialize_to(self, buf: Buffer, with_type: bool = True, with_name: bool = Tr @override @classmethod - def read_from(cls, buf: Buffer, with_type: bool = True, with_name: bool = True) -> _FloatingNBTag: + def read_from(cls, buf: Buffer, with_type: bool = True, with_name: bool = True) -> Self: name, tag_type = cls._read_header(buf, read_type=with_type, with_name=with_name) if _get_tag_type(cls) != tag_type: raise TypeError(f"Expected a {_get_tag_type(cls).name} tag, but found a different tag ({tag_type.name}).") @@ -629,10 +624,13 @@ def value(self) -> float: @override def validate(self) -> None: + if not isinstance(self.payload, (int, float)): # type: ignore # We want to check anyway + raise TypeError(f"Expected a float, but found {type(self.payload).__name__}.") + + @override + def transform(self) -> None: if isinstance(self.payload, int): self.payload = float(self.payload) - if not isinstance(self.payload, float): - raise TypeError(f"Expected a float, but found {type(self.payload).__name__}.") @final @@ -708,10 +706,13 @@ def value(self) -> bytes: @override def validate(self) -> None: + if not isinstance(self.payload, (bytearray, bytes)): + raise TypeError(f"Expected a bytes, but found {type(self.payload).__name__}.") + + @override + def transform(self) -> None: if isinstance(self.payload, bytearray): self.payload = bytes(self.payload) - if not isinstance(self.payload, bytes): - raise TypeError(f"Expected a bytes, but found {type(self.payload).__name__}.") @dataclass @@ -769,7 +770,7 @@ def validate(self) -> None: # Check that the string is valid UTF-8 try: self.payload.encode("utf-8") - except UnicodeEncodeError as exc: # pragma: no cover (don't know how to trigger) + except UnicodeEncodeError as exc: raise ValueError("Invalid UTF-8 string.") from exc @@ -790,21 +791,10 @@ def serialize_to(self, buf: Buffer, with_type: bool = True, with_name: bool = Tr IntNBT(0).serialize_to(buf, with_name=False, with_type=False) return - if not all(isinstance(tag, NBTag) for tag in self.payload): # type: ignore # We want to check anyway - raise ValueError( - f"All items in a list must be NBTags. Got {self.payload!r}.\nUse NBTag.from_object() to convert " - "objects to tags first." - ) - tag_type = _get_tag_type(self.payload[0]) ByteNBT(tag_type).serialize_to(buf, with_name=False, with_type=False) IntNBT(len(self.payload)).serialize_to(buf, with_name=False, with_type=False) for tag in self.payload: - if tag_type != _get_tag_type(tag): - raise ValueError(f"All tags in a list must be of the same type, got tag {tag!r}") - if tag.name != "": - raise ValueError(f"All tags in a list must be unnamed, got tag {tag!r}") - tag.serialize_to(buf, with_type=False, with_name=False) @override @@ -861,10 +851,7 @@ def to_object( result = result if not include_name else {self.name: result} if include_schema: subschemas = [ - cast( - "tuple[PayloadType, FromObjectSchema]", - tag.to_object(include_schema=True), - )[1] + cast("tuple[PayloadType, FromObjectSchema]", tag.to_object(include_schema=True))[1] for tag in self.payload ] if len(result) == 0: @@ -874,10 +861,13 @@ def to_object( if all(schema == first for schema in subschemas): return result, [first] - if not isinstance(first, (dict, list)): + # Useful tests but if they fail, this means `test_to_object_morecases` fails + # because they can only be triggered by a malfunction of to_object in a child class + # or the `validate` method + if not isinstance(first, (dict, list)): # pragma: no cover raise TypeError(f"The schema must contain either a dict or a list. Found {first!r}") # This will take care of ensuring either everything is a dict or a list - if not all(isinstance(schema, type(first)) for schema in subschemas): + if not all(isinstance(schema, type(first)) for schema in subschemas): # pragma: no cover raise TypeError(f"All items in the list must have the same type. Found {subschemas!r}") return result, subschemas return result @@ -915,17 +905,6 @@ def serialize_to(self, buf: Buffer, with_type: bool = True, with_name: bool = Tr if not self.payload: EndNBT().serialize_to(buf, with_name=False, with_type=True) return - if not all(isinstance(tag, NBTag) for tag in self.payload): # type: ignore # We want to check anyway - raise ValueError( - f"All items in a compound must be NBTags. Got {self.payload!r}.\n" - "Use NBTag.from_object() to convert objects to tags first." - ) - - if not all(tag.name for tag in self.payload): - raise ValueError(f"All tags in a compound must be named, got tags {self.payload!r}") - - if len(self.payload) != len({tag.name for tag in self.payload}): # Check for duplicate names - raise ValueError("All tags in a compound must have unique names.") for tag in self.payload: tag.serialize_to(buf) @@ -1026,9 +1005,11 @@ def validate(self) -> None: @dataclass -class _NumberArrayNBTag(NBTag): +class _NumberArrayNBTag(NBTag, RequiredParamsABCMixin): """Base class for NBT tags representing an array of numbers.""" + _REQUIRED_CLASS_VARS = ("STRUCT_FORMAT", "DATA_SIZE") + STRUCT_FORMAT: ClassVar[INT_FORMATS_TYPE] = NotImplemented # type: ignore DATA_SIZE: ClassVar[int] = NotImplemented @@ -1044,7 +1025,7 @@ def serialize_to(self, buf: Buffer, with_type: bool = True, with_name: bool = Tr @override @classmethod - def read_from(cls, buf: Buffer, with_type: bool = True, with_name: bool = True) -> _NumberArrayNBTag: + def read_from(cls, buf: Buffer, with_type: bool = True, with_name: bool = True) -> Self: name, tag_type = cls._read_header(buf, read_type=with_type, with_name=with_name) if _get_tag_type(cls) != tag_type: raise TypeError(f"Expected a {_get_tag_type(cls).name} tag, but found a different tag ({tag_type.name}).") @@ -1119,8 +1100,6 @@ def _get_tag_type(tag: NBTag | type[NBTag]) -> NBTagType: """Get the tag type of an NBTag object or class.""" cls = tag if isinstance(tag, type) else type(tag) - if cls is NBTag: - return NBTagType.COMPOUND for tag_type, tag_cls in ASSOCIATED_TYPES.items(): if cls is tag_cls: return tag_type diff --git a/mcproto/utils/abc.py b/mcproto/utils/abc.py index 5c7dcc64..728891d2 100644 --- a/mcproto/utils/abc.py +++ b/mcproto/utils/abc.py @@ -1,7 +1,7 @@ from __future__ import annotations import sys -from abc import ABC, ABCMeta, abstractmethod +from abc import ABC, abstractmethod from collections.abc import Sequence from dataclasses import dataclass as _dataclass from functools import partial @@ -45,7 +45,7 @@ class vars which should be defined on given class directly. That means inheritan __slots__ = () - _REQUIRRED_CLASS_VARS: ClassVar[Sequence[str]] + _REQUIRED_CLASS_VARS: ClassVar[Sequence[str]] _REQUIRED_CLASS_VARS_NO_MRO: ClassVar[Sequence[str]] def __new__(cls: type[Self], *a: Any, **kw: Any) -> Self: @@ -74,24 +74,7 @@ def __new__(cls: type[Self], *a: Any, **kw: Any) -> Self: return super().__new__(cls) -class _MetaDataclass(ABCMeta): - def __new__( - cls: type[_MetaDataclass], - name: str, - bases: tuple[type, ...], - namespace: dict[str, Any], - **kwargs: Any, - ) -> Any: # Create the class using the super() method to ensure it is correctly formed as an ABC - new_class = super().__new__(cls, name, bases, namespace, **kwargs) - - # Check if the dataclass is already defined, if not, create it - if not hasattr(new_class, "__dataclass_fields__"): - new_class = dataclass(new_class) - - return new_class - - -class Serializable(ABC): # , metaclass=_MetaDataclass): +class Serializable(ABC): """Base class for any type that should be (de)serializable into/from :class:`~mcproto.Buffer` data. Any class that inherits from this class and adds parameters should use the :func:`~mcproto.utils.abc.dataclass` @@ -103,10 +86,10 @@ class Serializable(ABC): # , metaclass=_MetaDataclass): def __post_init__(self) -> None: """Run the validation method after the object is initialized.""" self.validate() + self.transform() def serialize(self) -> Buffer: """Represent the object as a :class:`~mcproto.Buffer` (transmittable sequence of bytes).""" - self.validate() buf = Buffer() self.serialize_to(buf) return buf @@ -116,13 +99,29 @@ def serialize_to(self, buf: Buffer, /) -> None: """Write the object to a :class:`~mcproto.Buffer`.""" raise NotImplementedError + def transform(self) -> None: + """Apply a transformation to the payload of the object. + + This can be used to convert an int to an enum. This method is called during the initialization, + after the validation method. By default, this method does nothing. Override it in your subclass + to add transformation logic. + + This method should not raise any exception. + + .. note:: + This method is not called during serialization, any modifications made to the payload will have to + call this method manually to apply the transformation if needed. + """ + return + def validate(self) -> None: """Validate the object's attributes, raising an exception if they are invalid. - This will be called at the end of the object's initialization, and before serialization. - Use cast() in serialize_to() if your validation asserts that a value is of a certain type. - By default, this method does nothing. Override it in your subclass to add validation logic. + + .. note:: + This method is called before :meth:`~mcproto.utils.abc.Serializable.transform`, so it must not rely + on any transformations made by that method. """ return diff --git a/poetry.lock b/poetry.lock index b8215abe..69ba4ab4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -52,13 +52,13 @@ test = ["black (>=20.8b1)", "flake8 (>=3.8.3)", "mypy (>=0.812)", "mypy-extensio [[package]] name = "babel" -version = "2.14.0" +version = "2.15.0" description = "Internationalization utilities" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, - {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, + {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"}, + {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"}, ] [package.dependencies] @@ -300,63 +300,63 @@ files = [ [[package]] name = "coverage" -version = "7.5.0" +version = "7.5.1" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:432949a32c3e3f820af808db1833d6d1631664d53dd3ce487aa25d574e18ad1c"}, - {file = "coverage-7.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2bd7065249703cbeb6d4ce679c734bef0ee69baa7bff9724361ada04a15b7e3b"}, - {file = "coverage-7.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbfe6389c5522b99768a93d89aca52ef92310a96b99782973b9d11e80511f932"}, - {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39793731182c4be939b4be0cdecde074b833f6171313cf53481f869937129ed3"}, - {file = "coverage-7.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85a5dbe1ba1bf38d6c63b6d2c42132d45cbee6d9f0c51b52c59aa4afba057517"}, - {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:357754dcdfd811462a725e7501a9b4556388e8ecf66e79df6f4b988fa3d0b39a"}, - {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a81eb64feded34f40c8986869a2f764f0fe2db58c0530d3a4afbcde50f314880"}, - {file = "coverage-7.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:51431d0abbed3a868e967f8257c5faf283d41ec882f58413cf295a389bb22e58"}, - {file = "coverage-7.5.0-cp310-cp310-win32.whl", hash = "sha256:f609ebcb0242d84b7adeee2b06c11a2ddaec5464d21888b2c8255f5fd6a98ae4"}, - {file = "coverage-7.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:6782cd6216fab5a83216cc39f13ebe30adfac2fa72688c5a4d8d180cd52e8f6a"}, - {file = "coverage-7.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e768d870801f68c74c2b669fc909839660180c366501d4cc4b87efd6b0eee375"}, - {file = "coverage-7.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:84921b10aeb2dd453247fd10de22907984eaf80901b578a5cf0bb1e279a587cb"}, - {file = "coverage-7.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:710c62b6e35a9a766b99b15cdc56d5aeda0914edae8bb467e9c355f75d14ee95"}, - {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c379cdd3efc0658e652a14112d51a7668f6bfca7445c5a10dee7eabecabba19d"}, - {file = "coverage-7.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fea9d3ca80bcf17edb2c08a4704259dadac196fe5e9274067e7a20511fad1743"}, - {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:41327143c5b1d715f5f98a397608f90ab9ebba606ae4e6f3389c2145410c52b1"}, - {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:565b2e82d0968c977e0b0f7cbf25fd06d78d4856289abc79694c8edcce6eb2de"}, - {file = "coverage-7.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cf3539007202ebfe03923128fedfdd245db5860a36810136ad95a564a2fdffff"}, - {file = "coverage-7.5.0-cp311-cp311-win32.whl", hash = "sha256:bf0b4b8d9caa8d64df838e0f8dcf68fb570c5733b726d1494b87f3da85db3a2d"}, - {file = "coverage-7.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c6384cc90e37cfb60435bbbe0488444e54b98700f727f16f64d8bfda0b84656"}, - {file = "coverage-7.5.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fed7a72d54bd52f4aeb6c6e951f363903bd7d70bc1cad64dd1f087980d309ab9"}, - {file = "coverage-7.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cbe6581fcff7c8e262eb574244f81f5faaea539e712a058e6707a9d272fe5b64"}, - {file = "coverage-7.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad97ec0da94b378e593ef532b980c15e377df9b9608c7c6da3506953182398af"}, - {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd4bacd62aa2f1a1627352fe68885d6ee694bdaebb16038b6e680f2924a9b2cc"}, - {file = "coverage-7.5.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:adf032b6c105881f9d77fa17d9eebe0ad1f9bfb2ad25777811f97c5362aa07f2"}, - {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ba01d9ba112b55bfa4b24808ec431197bb34f09f66f7cb4fd0258ff9d3711b1"}, - {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f0bfe42523893c188e9616d853c47685e1c575fe25f737adf473d0405dcfa7eb"}, - {file = "coverage-7.5.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a9a7ef30a1b02547c1b23fa9a5564f03c9982fc71eb2ecb7f98c96d7a0db5cf2"}, - {file = "coverage-7.5.0-cp312-cp312-win32.whl", hash = "sha256:3c2b77f295edb9fcdb6a250f83e6481c679335ca7e6e4a955e4290350f2d22a4"}, - {file = "coverage-7.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:427e1e627b0963ac02d7c8730ca6d935df10280d230508c0ba059505e9233475"}, - {file = "coverage-7.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9dd88fce54abbdbf4c42fb1fea0e498973d07816f24c0e27a1ecaf91883ce69e"}, - {file = "coverage-7.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a898c11dca8f8c97b467138004a30133974aacd572818c383596f8d5b2eb04a9"}, - {file = "coverage-7.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07dfdd492d645eea1bd70fb1d6febdcf47db178b0d99161d8e4eed18e7f62fe7"}, - {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3d117890b6eee85887b1eed41eefe2e598ad6e40523d9f94c4c4b213258e4a4"}, - {file = "coverage-7.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6afd2e84e7da40fe23ca588379f815fb6dbbb1b757c883935ed11647205111cb"}, - {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a9960dd1891b2ddf13a7fe45339cd59ecee3abb6b8326d8b932d0c5da208104f"}, - {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ced268e82af993d7801a9db2dbc1d2322e786c5dc76295d8e89473d46c6b84d4"}, - {file = "coverage-7.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7c211f25777746d468d76f11719e64acb40eed410d81c26cefac641975beb88"}, - {file = "coverage-7.5.0-cp38-cp38-win32.whl", hash = "sha256:262fffc1f6c1a26125d5d573e1ec379285a3723363f3bd9c83923c9593a2ac25"}, - {file = "coverage-7.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:eed462b4541c540d63ab57b3fc69e7d8c84d5957668854ee4e408b50e92ce26a"}, - {file = "coverage-7.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d0194d654e360b3e6cc9b774e83235bae6b9b2cac3be09040880bb0e8a88f4a1"}, - {file = "coverage-7.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33c020d3322662e74bc507fb11488773a96894aa82a622c35a5a28673c0c26f5"}, - {file = "coverage-7.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbdf2cae14a06827bec50bd58e49249452d211d9caddd8bd80e35b53cb04631"}, - {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3235d7c781232e525b0761730e052388a01548bd7f67d0067a253887c6e8df46"}, - {file = "coverage-7.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2de4e546f0ec4b2787d625e0b16b78e99c3e21bc1722b4977c0dddf11ca84e"}, - {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4d0e206259b73af35c4ec1319fd04003776e11e859936658cb6ceffdeba0f5be"}, - {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2055c4fb9a6ff624253d432aa471a37202cd8f458c033d6d989be4499aed037b"}, - {file = "coverage-7.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:075299460948cd12722a970c7eae43d25d37989da682997687b34ae6b87c0ef0"}, - {file = "coverage-7.5.0-cp39-cp39-win32.whl", hash = "sha256:280132aada3bc2f0fac939a5771db4fbb84f245cb35b94fae4994d4c1f80dae7"}, - {file = "coverage-7.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c58536f6892559e030e6924896a44098bc1290663ea12532c78cef71d0df8493"}, - {file = "coverage-7.5.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:2b57780b51084d5223eee7b59f0d4911c31c16ee5aa12737c7a02455829ff067"}, - {file = "coverage-7.5.0.tar.gz", hash = "sha256:cf62d17310f34084c59c01e027259076479128d11e4661bb6c9acb38c5e19bb8"}, + {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, + {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, + {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, + {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, + {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, + {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, + {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, + {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, + {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, + {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, + {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, + {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, + {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, + {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, + {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, + {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, + {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, + {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, + {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, + {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, + {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, + {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, + {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, + {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, + {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, + {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, + {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, + {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, + {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, + {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, + {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, + {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, ] [package.dependencies] @@ -435,21 +435,18 @@ name = "docutils" version = "0.19" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=3.7" -files = [ - {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, - {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, -] +python-versions = "*" +files = [] [[package]] name = "dunamai" -version = "1.21.0" +version = "1.21.1" description = "Dynamic version generation" optional = false python-versions = ">=3.5" files = [ - {file = "dunamai-1.21.0-py3-none-any.whl", hash = "sha256:aceb7693e637f8114cbc547c818f746c953c8858c1023735efe3ec2d1f4b0571"}, - {file = "dunamai-1.21.0.tar.gz", hash = "sha256:28d4bf5e8e2f3fe5d6a4de61b05c4c70afaa7c6df55fe0204bb5b6b53761e4bc"}, + {file = "dunamai-1.21.1-py3-none-any.whl", hash = "sha256:fe303541463648b8197c495decf62cd8f15234fb6d891a5f295015e452f656c8"}, + {file = "dunamai-1.21.1.tar.gz", hash = "sha256:d7fea28ad2faf20a6ca5ec121e5c68e55eec6b8ada23d9c387e4e7a574cc559f"}, ] [package.dependencies] @@ -806,13 +803,13 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.1" +version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, - {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] @@ -913,17 +910,16 @@ files = [ [[package]] name = "pygments" -version = "2.17.2" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] @@ -1093,13 +1089,13 @@ files = [ [[package]] name = "requests" -version = "2.32.0" +version = "2.32.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ - {file = "requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5"}, - {file = "requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8"}, + {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, + {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, ] [package.dependencies] @@ -1155,19 +1151,18 @@ doc = ["Sphinx", "sphinx-rtd-theme"] [[package]] name = "setuptools" -version = "69.5.1" +version = "70.0.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, - {file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, + {file = "setuptools-70.0.0-py3-none-any.whl", hash = "sha256:54faa7f2e8d2d11bcd2c07bed282eef1046b5c080d1c32add737d7b5817b1ad4"}, + {file = "setuptools-70.0.0.tar.gz", hash = "sha256:f211a66637b8fa059bb28183da127d4e86396c991a942b028c6650d4319c3fd0"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "slotscheck" @@ -1440,13 +1435,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.4" +version = "0.12.5" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.4-py3-none-any.whl", hash = "sha256:5cd82d48a3dd89dee1f9d64420aa20ae65cfbd00668d6f094d7578a78efbb77b"}, - {file = "tomlkit-0.12.4.tar.gz", hash = "sha256:7ca1cfc12232806517a8515047ba66a19369e71edf2439d0f5824f91032b6cc3"}, + {file = "tomlkit-0.12.5-py3-none-any.whl", hash = "sha256:af914f5a9c59ed9d0762c7b64d3b5d5df007448eb9cd2edc8a46b1eafead172f"}, + {file = "tomlkit-0.12.5.tar.gz", hash = "sha256:eef34fba39834d4d6b73c9ba7f3e4d1c417a4e56f89a7e96e090dd0d24b8fb3c"}, ] [[package]] @@ -1500,13 +1495,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.26.1" +version = "20.26.2" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.26.1-py3-none-any.whl", hash = "sha256:7aa9982a728ae5892558bff6a2839c00b9ed145523ece2274fad6f414690ae75"}, - {file = "virtualenv-20.26.1.tar.gz", hash = "sha256:604bfdceaeece392802e6ae48e69cec49168b9c5f4a44e483963f9242eb0e78b"}, + {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, + {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, ] [package.dependencies] @@ -1520,20 +1515,20 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "zipp" -version = "3.18.1" +version = "3.18.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, - {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, + {file = "zipp-3.18.2-py3-none-any.whl", hash = "sha256:dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e"}, + {file = "zipp-3.18.2.tar.gz", hash = "sha256:6278d9ddbcfb1f1089a88fde84481528b07b0e10474e09dcfe53dad4069fa059"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<4" -content-hash = "33f308172547bdcc8b8eecb3b6a90fda566c0d13134073c3a9c66bc69d659070" +content-hash = "f4cb5246478564d84b0b663132a8841326e52cf36bfd91e264fd9cba68a89250" diff --git a/pyproject.toml b/pyproject.toml index 7a214763..c23394ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ sphinx-autodoc-typehints = ">=1.23,<3.0" sphinx-copybutton = "^0.5.2" furo = ">=2022.12.7" sphinxcontrib-towncrier = ">=0.3.2,<0.5.0" +pytest = "^7.3.1" # Required to list the gen_test_serializable function in the docs [tool.poetry.group.docs-ci] optional = true diff --git a/tests/helpers.py b/tests/helpers.py index 148fe973..19b2b3b1 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -4,7 +4,7 @@ import inspect import unittest.mock from collections.abc import Callable, Coroutine -from typing import Any, Dict, Generic, Tuple, TypeVar, cast +from typing import Any, Generic, Literal, TypeVar, Union, cast, overload import pytest from typing_extensions import ParamSpec, override @@ -168,12 +168,54 @@ def __init__(self, **kwargs): super().__init__(spec_set=self.spec_set, **kwargs) # type: ignore # Mixin class, this __init__ is valid +@overload +def isexception(obj: type[Exception] | Exception) -> Literal[True]: ... # type: ignore[reportOverlappingOverload] +@overload +def isexception(obj: Any) -> Literal[False]: ... + + +def isexception(obj: Any) -> bool: + """Check if the object is an exception.""" + return (isinstance(obj, type) and issubclass(obj, Exception)) or isinstance(obj, Exception) + + +def check_exception_equality(raised: Exception, expected: type[Exception] | Exception) -> tuple[bool, str]: + """Check if two exceptions are equal (compatible). + + This function is used to check if the raised exception is compatible with the expected one, meaning + that if Excepted is a type, the raised exception must be an instance of that type. If Expected is an instance + of an exception then the type must be equal, but also the message and the arguments must be equal. + + :param raised: The raised exception. + :param expected: The expected exception. + + :return: Whether the exceptions are compatible. + """ + if isinstance(expected, type): + return isinstance( + raised, expected + ), f"Did not raise the expected exception: expected {expected!r}, got {raised!r}" + + if type(raised) != type(expected): + return check_exception_equality(raised, type(expected)) # Reuse the message from the type check + + if raised.args != expected.args: + return False, f"Raised exception has different arguments: expected {expected.args}, got {raised.args!r}" + + if str(raised) != str(expected): + return False, f"Raised exception has different message: expected {expected}, got {raised}" + + return True, "" + + def gen_serializable_test( context: dict[str, Any], cls: type[Serializable], - fields: list[tuple[str, type]], + fields: list[tuple[str, type | str]], test_data: list[ - tuple[tuple[Any, ...], bytes] | tuple[tuple[Any, ...], type[Exception]] | tuple[type[Exception], bytes] + tuple[tuple[Any, ...], bytes] + | tuple[tuple[Any, ...], type[Exception] | Exception] + | tuple[type[Exception] | Exception, bytes] ], ): """Generate tests for a serializable class. @@ -186,73 +228,41 @@ def gen_serializable_test( :param fields: A list of tuples containing the field names and types of the serializable class. :param test_data: A list of test data. Each element is a tuple containing either: - A tuple of parameters to pass to the serializable class constructor and the expected bytes after - serialization + serialization - A tuple of parameters to pass to the serializable class constructor and the expected exception during - validation + validation - An exception to expect during deserialization and the bytes to deserialize Example usage: - ```python - @final - @dataclass - class ToyClass(Serializable): - a: int - b: str - - def serialize_to(self, buf: Buffer): - buf.write_varint(self.a) - buf.write_utf(self.b) - - @classmethod - def deserialize(cls, buf: Buffer) -> "ToyClass": - a = buf.read_varint() - b = buf.read_utf() - if len(b) > 10: - raise ValueError("b must be less than 10 characters") - return cls(a, b) - - def validate(self) -> None: - if self.a == 0: - raise ZeroDivisionError("a must be non-zero") - - 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), - (IOError, b"\x01"), # Not enough data to deserialize - (ValueError, b"\x01\x0bhello world"), # 0b = 11 is too long - ], - ) - ``` - This will add 1 class test with 4 test functions containing the tests for serialization, deserialization, - validation, and deserialization error handling + .. literalinclude:: /../tests/mcproto/utils/test_serializable.py + :start-after: # region ToyClass + :linenos: + :language: python + This will add 1 class test with 4 test functions containing the tests for serialization, deserialization, + validation, and deserialization error handling """ # Separate the test data into parameters for each test function # This holds the parameters for the serialization and deserialization tests parameters: list[tuple[dict[str, Any], bytes]] = [] # This holds the parameters for the validation tests - validation_fail: list[tuple[dict[str, Any], type[Exception]]] = [] + validation_fail: list[tuple[dict[str, Any], type[Exception] | Exception]] = [] # This holds the parameters for the deserialization error tests - deserialization_fail: list[tuple[bytes, type[Exception]]] = [] + deserialization_fail: list[tuple[bytes, type[Exception] | Exception]] = [] for data_or_exc, expected_bytes_or_exc in test_data: if isinstance(data_or_exc, tuple) and isinstance(expected_bytes_or_exc, bytes): kwargs = dict(zip([f[0] for f in fields], data_or_exc)) parameters.append((kwargs, expected_bytes_or_exc)) - elif isinstance(data_or_exc, type) and isinstance(expected_bytes_or_exc, bytes): - deserialization_fail.append((expected_bytes_or_exc, data_or_exc)) - else: - data = cast(Tuple[Any, ...], data_or_exc) - exception = cast(type[Exception], expected_bytes_or_exc) - kwargs = dict(zip([f[0] for f in fields], data)) + elif isexception(data_or_exc) and isinstance(expected_bytes_or_exc, bytes): + exception = cast(Union[type[Exception], Exception], data_or_exc) + deserialization_fail.append((expected_bytes_or_exc, exception)) + elif isinstance(data_or_exc, tuple) and isexception(expected_bytes_or_exc): + exception = cast(Union[type[Exception], Exception], expected_bytes_or_exc) + kwargs = dict(zip([f[0] for f in fields], data_or_exc)) validation_fail.append((kwargs, exception)) def generate_name(param: dict[str, Any] | bytes, i: int) -> str: @@ -260,14 +270,12 @@ def generate_name(param: dict[str, Any] | bytes, i: int) -> str: length = 30 result = f"{i:02d}] : " # the first [ is added by pytest if isinstance(param, bytes): - try: - result += str(param[:length], "utf-8") + "..." if len(param) > (length + 3) else param.decode("utf-8") - except UnicodeDecodeError: - result += repr(param[:length]) + "..." if len(param) > (length + 3) else repr(param) - else: - param = cast(Dict[str, Any], param) - begin = ", ".join(f"{k}={v}" for k, v in param.items()) + result += repr(param[:length]) + "..." if len(param) > (length + 3) else repr(param) + elif isinstance(param, dict): + begin = ", ".join(f"{k}={v!r}" for k, v in param.items()) result += begin[:length] + "..." if len(begin) > (length + 3) else begin + else: + raise TypeError(f"Wrong type for param : {param!r}") result = result.replace("\n", "\\n").replace("\r", "\\r") result += f" [{cls.__name__}" # the other [ is added by pytest return result @@ -283,7 +291,7 @@ class TestClass: def test_serialization(self, kwargs: dict[str, Any], expected_bytes: bytes): """Test serialization of the object.""" obj = cls(**kwargs) - serialized_bytes = obj.serialize() + serialized_bytes = bytes(obj.serialize()) assert serialized_bytes == expected_bytes, f"{serialized_bytes} != {expected_bytes}" @pytest.mark.parametrize( @@ -295,7 +303,21 @@ def test_deserialization(self, kwargs: dict[str, Any], expected_bytes: bytes): """Test deserialization of the object.""" buf = Buffer(expected_bytes) obj = cls.deserialize(buf) - assert cls(**kwargs) == obj, f"{cls.__name__}({kwargs}) != {obj}" + equality = cls(**kwargs) == obj + error_message: list[str] = [] + # Try to find the mismatched field + if not equality: + for field, value in kwargs.items(): + obj_val = getattr(obj, field, None) + if obj_val is None: # Either skip it, or it is intended to be None + continue + if obj_val != value: + error_message.append(f"{field}={obj_val} != {value}") + break + if error_message: + assert equality, f"Object not equal: {', '.join(error_message)}" + else: + assert equality, f"Object not equal: {obj} != {cls(**kwargs)} (expected)" assert buf.remaining == 0, f"Buffer has {buf.remaining} bytes remaining" @pytest.mark.parametrize( @@ -303,21 +325,31 @@ def test_deserialization(self, kwargs: dict[str, Any], expected_bytes: bytes): validation_fail, ids=tuple(generate_name(kwargs, i) for i, (kwargs, _) in enumerate(validation_fail)), ) - def test_validation(self, kwargs: dict[str, Any], exc: type[Exception]): + def test_validation(self, kwargs: dict[str, Any], exc: type[Exception] | Exception): """Test validation of the object.""" - with pytest.raises(exc): + try: cls(**kwargs) + except Exception as e: # noqa: BLE001 # We do want to catch all exceptions + passed, message = check_exception_equality(e, exc) + assert passed, message + else: + raise AssertionError(f"Expected {exc} to be raised") @pytest.mark.parametrize( ("content", "exc"), deserialization_fail, ids=tuple(generate_name(content, i) for i, (content, _) in enumerate(deserialization_fail)), ) - def test_deserialization_error(self, content: bytes, exc: type[Exception]): + def test_deserialization_error(self, content: bytes, exc: type[Exception] | Exception): """Test deserialization error handling.""" buf = Buffer(content) - with pytest.raises(exc): + try: cls.deserialize(buf) + except Exception as e: # noqa: BLE001 # We do want to catch all exceptions + passed, message = check_exception_equality(e, exc) + assert passed, message + else: + raise AssertionError(f"Expected {exc} to be raised") if len(parameters) == 0: # If there are no serialization tests, remove them @@ -336,4 +368,4 @@ def test_deserialization_error(self, content: bytes, exc: type[Exception]): TestClass.__name__ = f"TestGen{cls.__name__}" # Add the test functions to the global context - context[TestClass.__name__] = TestClass + context[TestClass.__name__] = TestClass # BERK diff --git a/tests/mcproto/packets/handshaking/test_handshake.py b/tests/mcproto/packets/handshaking/test_handshake.py index 0e34952f..53c5de4a 100644 --- a/tests/mcproto/packets/handshaking/test_handshake.py +++ b/tests/mcproto/packets/handshaking/test_handshake.py @@ -31,8 +31,5 @@ ), # Invalid next state ((757, "localhost", 25565, 3), ValueError), - ((757, "localhost", 25565, 4), ValueError), - ((757, "localhost", 25565, 5), ValueError), - ((757, "localhost", 25565, 6), ValueError), ], ) diff --git a/tests/mcproto/packets/status/test_status.py b/tests/mcproto/packets/status/test_status.py index bd40a31b..19ce720c 100644 --- a/tests/mcproto/packets/status/test_status.py +++ b/tests/mcproto/packets/status/test_status.py @@ -1,6 +1,5 @@ from __future__ import annotations -from typing import Any, Dict from mcproto.packets.status.status import StatusResponse from tests.helpers import gen_serializable_test @@ -8,7 +7,7 @@ gen_serializable_test( context=globals(), cls=StatusResponse, - fields=[("data", Dict[str, Any])], + fields=[("data", "dict[str, Any]")], test_data=[ ( ( @@ -19,15 +18,10 @@ }, ), bytes.fromhex( - "84017b226465736372697074696f6e223a207b2274657874223a202241204" - "d696e65637261667420536572766572227d2c2022706c6179657273223a20" - "7b226d6178223a2032302c20226f6e6c696e65223a20307d2c20227665727" - "3696f6e223a207b226e616d65223a2022312e31382e31222c202270726f74" - "6f636f6c223a203735377d7d" - # Contains spaces that are not present in the expected bytes. - # "5637261667420536572766572227d2c22706c6179657273223a7b226d6178" - # "223a32302c226f6e6c696e65223a20307d2c2276657273696f6e223a7b226" - # "e616d65223a22312e31382e31222c2270726f746f636f6c223a3735377d7d" + "787b226465736372697074696f6e223a7b2274657874223a2241204d696e6" + "5637261667420536572766572227d2c22706c6179657273223a7b226d6178" + "223a32302c226f6e6c696e65223a307d2c2276657273696f6e223a7b226e6" + "16d65223a22312e31382e31222c2270726f746f636f6c223a3735377d7d" ), ), # Unserializable data for JSON diff --git a/tests/mcproto/types/test_nbt.py b/tests/mcproto/types/test_nbt.py index 671f1707..3447e090 100644 --- a/tests/mcproto/types/test_nbt.py +++ b/tests/mcproto/types/test_nbt.py @@ -1,7 +1,7 @@ from __future__ import annotations import struct -from typing import Any, Dict, List, cast +from typing import Any, List, cast import pytest @@ -228,6 +228,8 @@ (("a" * 32768, "b"), ValueError), # Wrong type ((1, "a"), TypeError), + # Unpaired surrogate + (("\udc80", "a"), ValueError), ], ) @@ -486,8 +488,8 @@ def check_equality(self: object, other: object) -> bool: if type(self) != type(other): return False if isinstance(self, dict): - self = cast(Dict[Any, Any], self) - other = cast(Dict[Any, Any], other) + self = cast("dict[Any, Any]", self) + other = cast("dict[Any, Any]", other) if len(self) != len(other): return False for key in self: @@ -646,7 +648,7 @@ def to_nbt(self, name: str = "") -> NBTag: {"test": object()}, ByteNBT, TypeError, - "Expected one of \\(bytes, str, int, float, list\\), but found object.", + r"Expected one of \(bytes, str, int, float, list\), but found object.", ), # The data is a list but not all elements are ints ( diff --git a/tests/mcproto/utils/test_serializable.py b/tests/mcproto/utils/test_serializable.py index 2b1b0fbd..08200146 100644 --- a/tests/mcproto/utils/test_serializable.py +++ b/tests/mcproto/utils/test_serializable.py @@ -1,6 +1,8 @@ from __future__ import annotations -from typing import final +import math + +from typing import cast, final from typing_extensions import override from mcproto.buffer import Buffer @@ -8,17 +10,19 @@ from tests.helpers import gen_serializable_test +# region ToyClass @final @dataclass class ToyClass(Serializable): """Toy class for testing demonstrating the use of gen_serializable_test on `Serializable`.""" a: int - b: str + 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) @@ -37,10 +41,19 @@ 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: + 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) + + +# endregion +# region Test ToyClass gen_serializable_test( context=globals(), cls=ToyClass, @@ -48,9 +61,13 @@ def validate(self) -> None: test_data=[ ((1, "hello"), b"\x01\x05hello"), ((2, "world"), b"\x02\x05world"), - ((0, "hello"), ZeroDivisionError), - ((1, "hello world"), ValueError), + ((3, 1234567890), b"\x03\x0a1234567890"), + ((0, "hello"), ZeroDivisionError("a must be non-zero")), # Message specified + ((1, "hello world"), ValueError), # No message specified + ((1, 12345678900), ValueError), (ZeroDivisionError, b"\x00"), + (ZeroDivisionError, b"\x00\x05hello"), (IOError, b"\x01"), ], ) +# endregion