diff --git a/poetry.lock b/poetry.lock index 60989c7..7ccd53c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -172,6 +172,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "nodeenv" +version = "1.6.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "packaging" version = "21.3" @@ -261,6 +269,22 @@ python-versions = ">=3.6" [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pyright" +version = "1.1.225" +description = "Command line wrapper for pyright" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +nodeenv = ">=1.6.0" +typing-extensions = {version = ">=3.7", markers = "python_version < \"3.8\""} + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + [[package]] name = "pytest" version = "7.0.1" @@ -337,7 +361,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "3750687b24a352b625441ebaf1d20726351e3ebfc592fb386e4491455582ae49" +content-hash = "805914111993bc904b628a157e6c4dd0cf1e3e596b34642c13f977c941487777" [metadata.files] atomicwrites = [ @@ -494,6 +518,10 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +nodeenv = [ + {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, + {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, +] packaging = [ {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, @@ -530,6 +558,10 @@ pyparsing = [ {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"}, {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"}, ] +pyright = [ + {file = "pyright-1.1.225-py3-none-any.whl", hash = "sha256:ebb6de095972914b6b3b12d053474e29943389f585dd4bff8a684eb45c4d6987"}, + {file = "pyright-1.1.225.tar.gz", hash = "sha256:d638b616bdaea762502e8904307ca8247754d7629b7adf05f0bb2bf64cbb340f"}, +] pytest = [ {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, diff --git a/pymine_net/net/asyncio/client.py b/pymine_net/net/asyncio/client.py index ef7f50f..2712384 100644 --- a/pymine_net/net/asyncio/client.py +++ b/pymine_net/net/asyncio/client.py @@ -15,7 +15,11 @@ class AsyncProtocolClient(AbstractProtocolClient): def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: PacketMap): super().__init__(host, port, protocol, packet_map) - self.stream: AsyncTCPStream = None + # We type-ignore this assignment since we don't expect it to stay as None + # it should be set in connect function which is expected to be ran after init + # this avoids further type-ignores or casts whenever it'd be used, to tell + # type checker that it won't actually be None. + self.stream: AsyncTCPStream = None # type: ignore async def connect(self) -> None: _, writer = await asyncio.open_connection(self.host, self.port) diff --git a/pymine_net/net/asyncio/server.py b/pymine_net/net/asyncio/server.py index e3882b6..dd236b3 100644 --- a/pymine_net/net/asyncio/server.py +++ b/pymine_net/net/asyncio/server.py @@ -30,7 +30,11 @@ def __init__(self, host: str, port: int, protocol: Union[int, str], packet_map: self.connected_clients: Dict[Tuple[str, int], AsyncProtocolServerClient] = {} - self.server: asyncio.AbstractServer = None + # We type-ignore this assignment since we don't expect it to stay as None + # it should be set in run function which is expected to be ran after init + # this avoids further type-ignores or casts whenever it'd be used, to tell + # type checker that it won't actually be None. + self.server: asyncio.AbstractServer = None # type: ignore async def run(self) -> None: self.server = await asyncio.start_server(self._client_connected_cb, self.host, self.port) diff --git a/pymine_net/net/server.py b/pymine_net/net/server.py index ef0b1b7..d9c8d07 100644 --- a/pymine_net/net/server.py +++ b/pymine_net/net/server.py @@ -46,7 +46,7 @@ def _decode_packet(self, buf: Buffer) -> ServerBoundPacket: # attempt to get packet class from given state and packet id try: - packet_class: Type[ClientBoundPacket] = self.packet_map[ + packet_class: Type[ServerBoundPacket] = self.packet_map[ PacketDirection.SERVERBOUND, self.state, packet_id ] except KeyError: diff --git a/pymine_net/packets/v_1_18_1/login/login.py b/pymine_net/packets/v_1_18_1/login/login.py index 36774b1..1473718 100644 --- a/pymine_net/packets/v_1_18_1/login/login.py +++ b/pymine_net/packets/v_1_18_1/login/login.py @@ -2,6 +2,7 @@ from __future__ import annotations +from typing import Optional from uuid import UUID from pymine_net.types.buffer import Buffer @@ -190,7 +191,7 @@ class LoginPluginResponse(ServerBoundPacket): id = 0x02 - def __init__(self, message_id: int, data: bytes = None): + def __init__(self, message_id: int, data: Optional[bytes] = None): self.message_id = message_id self.data = data diff --git a/pymine_net/packets/v_1_18_1/play/advancement.py b/pymine_net/packets/v_1_18_1/play/advancement.py index aaa0db8..0e95cbf 100644 --- a/pymine_net/packets/v_1_18_1/play/advancement.py +++ b/pymine_net/packets/v_1_18_1/play/advancement.py @@ -41,7 +41,7 @@ class PlaySelectAdvancementTab(ClientBoundPacket): id = 0x40 - def __init__(self, identifier: str = None): + def __init__(self, identifier: Optional[str] = None): super().__init__() self.identifier = identifier diff --git a/pymine_net/packets/v_1_18_1/play/boss.py b/pymine_net/packets/v_1_18_1/play/boss.py index e88ef05..a8bb85a 100644 --- a/pymine_net/packets/v_1_18_1/play/boss.py +++ b/pymine_net/packets/v_1_18_1/play/boss.py @@ -32,7 +32,7 @@ def __init__(self, uuid: UUID, action: int, **data: dict): self.data = data def pack(self) -> Buffer: - buf = Buffer.write_uuid(self.uuid).write_varint(self.action) + buf = Buffer().write_uuid(self.uuid).write_varint(self.action) if self.action == 0: buf.write_chat(self.data["title"]).write("f", self.data["health"]).write_varint( diff --git a/pymine_net/packets/v_1_18_1/play/crafting.py b/pymine_net/packets/v_1_18_1/play/crafting.py index 50a7b43..d4d04a0 100644 --- a/pymine_net/packets/v_1_18_1/play/crafting.py +++ b/pymine_net/packets/v_1_18_1/play/crafting.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Dict, List +from typing import Dict, List, Optional from pymine_net.types.buffer import Buffer from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket @@ -177,7 +177,7 @@ def __init__( smoker_book_open: bool, smoker_book_filter_active: bool, recipe_ids_1: List[str], - recipe_ids_2: List[list] = None, + recipe_ids_2: Optional[List[list]] = None, ): super().__init__() diff --git a/pymine_net/packets/v_1_18_1/play/map.py b/pymine_net/packets/v_1_18_1/play/map.py index 89d5565..a1b70e4 100644 --- a/pymine_net/packets/v_1_18_1/play/map.py +++ b/pymine_net/packets/v_1_18_1/play/map.py @@ -47,10 +47,10 @@ def __init__( tracking_pos: bool, icons: List[Tuple[int, int, int, int, bool, Optional[Chat]]], columns: int, - rows: int = None, - x: int = None, - z: int = None, - data: bytes = None, + rows: Optional[int] = None, + x: Optional[int] = None, + z: Optional[int] = None, + data: Optional[bytes] = None, ): super().__init__() @@ -86,7 +86,7 @@ def pack(self) -> Buffer: buf.write("B", self.columns) - if len(self.columns) < 1: + if self.columns < 1: return buf return ( diff --git a/pymine_net/packets/v_1_18_1/play/particle.py b/pymine_net/packets/v_1_18_1/play/particle.py index e5aa490..bcd3fce 100644 --- a/pymine_net/packets/v_1_18_1/play/particle.py +++ b/pymine_net/packets/v_1_18_1/play/particle.py @@ -73,5 +73,5 @@ def pack(self) -> Buffer: .write("f", self.offset_z) .write("f", self.particle_data) .write("i", self.particle_count) - .write_particle(self.data) + .write_particle(**self.data) ) diff --git a/pymine_net/packets/v_1_18_1/play/player.py b/pymine_net/packets/v_1_18_1/play/player.py index d5bbd5b..5b8554d 100644 --- a/pymine_net/packets/v_1_18_1/play/player.py +++ b/pymine_net/packets/v_1_18_1/play/player.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import List +from typing import List, Optional, cast from uuid import UUID import pymine_net.types.nbt as nbt @@ -314,7 +314,12 @@ def __init__(self, x: float, feet_y: float, z: float, on_ground: bool): @classmethod def unpack(cls, buf: Buffer) -> PlayPlayerPositionServerBound: - return cls(buf.read("d"), buf.read("d"), buf.read("d"), buf.read("?")) + return cls( + cast(int, buf.read("d")), + cast(int, buf.read("d")), + cast(int, buf.read("d")), + cast(bool, buf.read("?")) + ) class PlayPlayerPositionAndRotationServerBound(ServerBoundPacket): @@ -352,12 +357,12 @@ def __init__( @classmethod def unpack(cls, buf: Buffer) -> PlayPlayerPositionAndRotationServerBound: return cls( - buf.read("d"), - buf.read("d"), - buf.read("d"), - buf.read("f"), - buf.read("f"), - buf.read("?"), + cast(int, buf.read("d")), + cast(int, buf.read("d")), + cast(int, buf.read("d")), + cast(float, buf.read("f")), + cast(float, buf.read("f")), + cast(bool, buf.read("?")), ) @@ -416,7 +421,11 @@ def __init__(self, yaw: float, pitch: float, on_ground: bool): @classmethod def unpack(cls, buf: Buffer) -> PlayPlayerRotation: - return cls(buf.read("f"), buf.read("f"), buf.read("?")) + return cls( + cast(float, buf.read("f")), + cast(float, buf.read("f")), + cast(bool, buf.read("?")) + ) class PlayPlayerMovement(ServerBoundPacket): @@ -436,7 +445,7 @@ def __init__(self, on_ground: bool): @classmethod def unpack(cls, buf: Buffer) -> PlayPlayerMovement: - return cls(buf.read("?")) + return cls(cast(bool, buf.read("?"))) class PlayTeleportConfirm(ServerBoundPacket): @@ -530,11 +539,11 @@ def unpack(cls, buf: Buffer) -> PlayClientSettings: buf.read_string(), buf.read_byte(), buf.read_varint(), - buf.read("?"), - buf.read("B"), + cast(bool, buf.read("?")), + cast(int, buf.read("B")), buf.read_varint(), - buf.read("?"), - buf.read("?"), + cast(bool, buf.read("?")), + cast(bool, buf.read("?")), ) @@ -558,7 +567,10 @@ def __init__(self, slot_id: int, slot: dict): @classmethod def unpack(cls, buf: Buffer) -> PlayCreativeInventoryAction: - return cls(buf.read("h"), buf.read_slot()) + return cls( + cast(int, buf.read("h")), + buf.read_slot(), # TODO: This is missing a Registry parameter? + ) class PlaySpectate(ServerBoundPacket): @@ -775,6 +787,8 @@ def pack(self) -> Buffer: for player in self.players: buf.write_uuid(player.uuid) + return buf + class PlayFacePlayer(ClientBoundPacket): """Used by the server to rotate the client player to face the given location or entity. (Server -> Client) @@ -804,8 +818,8 @@ def __init__( target_y: float, target_z: float, is_entity: bool, - entity_id: int = None, - entity_feet_or_eyes: int = None, + entity_id: Optional[int] = None, + entity_feet_or_eyes: Optional[int] = None, ): super().__init__() diff --git a/pymine_net/types/block_palette.py b/pymine_net/types/block_palette.py index 7a0e08b..d745a2a 100644 --- a/pymine_net/types/block_palette.py +++ b/pymine_net/types/block_palette.py @@ -1,4 +1,5 @@ from abc import abstractmethod +from typing import Optional from strict_abc import StrictABC @@ -17,7 +18,7 @@ def get_bits_per_block(self) -> int: pass @abstractmethod - def encode(self, block: str, props: dict = None) -> int: + def encode(self, block: str, props: Optional[dict] = None) -> int: pass @abstractmethod diff --git a/pymine_net/types/buffer.py b/pymine_net/types/buffer.py index fdba5e7..92b0459 100644 --- a/pymine_net/types/buffer.py +++ b/pymine_net/types/buffer.py @@ -4,13 +4,19 @@ import struct import uuid from functools import partial -from typing import Callable, Dict, Optional, Tuple, Union +from typing import TYPE_CHECKING, Callable, Dict, Optional, Tuple, TypeVar, Union, cast from pymine_net.enums import Direction, EntityModifier, Pose from pymine_net.types import nbt from pymine_net.types.chat import Chat from pymine_net.types.registry import Registry +if TYPE_CHECKING: + from typing_extensions import Self, TypeAlias + +T = TypeVar("T") +JsonCompatible: TypeAlias = Union[dict, list, str, int, float, bool, None] + __all__ = ("Buffer",) @@ -21,12 +27,12 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.pos = 0 - def write_bytes(self, data: Union[bytes, bytearray]) -> Buffer: + def write_bytes(self, data: Union[bytes, bytearray]) -> Self: """Writes bytes to the buffer.""" return self.extend(data) - def read_bytes(self, length: int = None) -> bytearray: + def read_bytes(self, length: Optional[int] = None) -> bytearray: """Reads bytes from the buffer, if length is None then all bytes are read.""" if length is None: @@ -48,7 +54,7 @@ def reset(self) -> None: self.pos = 0 - def extend(self, data: Union[Buffer, bytes, bytearray]) -> Buffer: + def extend(self, data: Union[bytes, bytearray]) -> Self: super().extend(data) return self @@ -59,7 +65,7 @@ def read_byte(self) -> int: self.pos += 1 return byte - def write_byte(self, value: int) -> Buffer: + def write_byte(self, value: int) -> Self: """Writes a singular byte passed as an integer to the buffer.""" return self.extend(struct.pack(">b", value)) @@ -74,19 +80,19 @@ def read(self, fmt: str) -> Union[object, Tuple[object]]: return unpacked - def write(self, fmt: str, *value: object) -> Buffer: + def write(self, fmt: str, *value: object) -> Self: """Using the given format and value, packs the value and writes it to the buffer.""" self.write_bytes(struct.pack(">" + fmt, *value)) return self - def read_optional(self, reader: Callable) -> Optional[object]: + def read_optional(self, reader: Callable[[], T]) -> Optional[T]: """Reads an optional value from the buffer.""" if self.read("?"): return reader() - def write_optional(self, writer: Callable, value: object = None) -> Buffer: + def write_optional(self, writer: Callable, value: Optional[object] = None) -> Self: """Writes an optional value to the buffer.""" if value is None: @@ -103,7 +109,7 @@ def read_varint(self, max_bits: int = 32) -> int: value = 0 for i in range(10): - byte = self.read("B") + byte = cast(int, self.read("B")) value |= (byte & 0x7F) << 7 * i if not byte & 0x80: @@ -122,7 +128,7 @@ def read_varint(self, max_bits: int = 32) -> int: return value - def write_varint(self, value: int, max_bits: int = 32) -> Buffer: + def write_varint(self, value: int, max_bits: int = 32) -> Self: """Writes a varint to the buffer.""" value_max = (1 << (max_bits - 1)) - 1 @@ -157,7 +163,7 @@ def read_optional_varint(self) -> Optional[int]: return value - 1 - def write_optional_varint(self, value: int = None) -> Buffer: + def write_optional_varint(self, value: Optional[int] = None) -> Self: """Writes an optional (None if not present) varint to the buffer.""" return self.write_varint(0 if value is None else value + 1) @@ -167,7 +173,7 @@ def read_string(self) -> str: return self.read_bytes(self.read_varint(max_bits=16)).decode("utf-8") - def write_string(self, value: str) -> Buffer: + def write_string(self, value: str) -> Self: """Writes a string in UTF8 to the buffer.""" encoded = value.encode("utf-8") @@ -175,12 +181,12 @@ def write_string(self, value: str) -> Buffer: return self - def read_json(self) -> object: + def read_json(self) -> JsonCompatible: """Reads json data from the buffer.""" return json.loads(self.read_string()) - def write_json(self, value: object) -> Buffer: + def write_json(self, value: JsonCompatible) -> Self: """Writes json data to the buffer.""" return self.write_string(json.dumps(value)) @@ -190,7 +196,7 @@ def read_nbt(self) -> nbt.TAG_Compound: return nbt.unpack(self[self.pos :]) - def write_nbt(self, value: nbt.TAG = None) -> Buffer: + def write_nbt(self, value: Optional[nbt.TAG] = None) -> Self: """Writes an nbt tag to the buffer.""" if value is None: @@ -205,7 +211,7 @@ def read_uuid(self) -> uuid.UUID: return uuid.UUID(bytes=bytes(self.read_bytes(16))) - def write_uuid(self, value: uuid.UUID) -> Buffer: + def write_uuid(self, value: uuid.UUID) -> Self: """Writes a UUID to the buffer.""" return self.write_bytes(value.bytes) @@ -219,7 +225,7 @@ def from_twos_complement(num, bits): return num - data = self.read("Q") + data = cast(int, self.read("Q")) return ( from_twos_complement(data >> 38, 26), @@ -232,12 +238,12 @@ def read_chat(self) -> Chat: return Chat(self.read_json()) - def write_chat(self, value: Chat) -> Buffer: + def write_chat(self, value: Chat) -> Self: """Writes a chat message to the buffer.""" return self.write_json(value.data) - def write_position(self, x: int, y: int, z: int) -> Buffer: + def write_position(self, x: int, y: int, z: int) -> Self: """Writes a Minecraft position (x, y, z) to the buffer.""" def to_twos_complement(num, bits): @@ -264,20 +270,21 @@ def read_slot(self, registry: Registry) -> dict: "tag": self.read_nbt(), } - def write_slot(self, item_id: int = None, count: int = 1, tag: nbt.TAG = None) -> Buffer: + def write_slot( + self, item_id: Optional[int] = None, count: int = 1, tag: Optional[nbt.TAG] = None + ) -> Self: """Writes an inventory / container slot to the buffer.""" if item_id is None: - self.write("?", False) - else: - self.write("?", True).write_varint(item_id).write("b", count).write_nbt(tag) + return self.write("?", False) + return self.write("?", True).write_varint(item_id).write("b", count).write_nbt(tag) def read_rotation(self) -> Tuple[float, float, float]: """Reads a rotation from the buffer.""" - return self.read("fff") + return cast(Tuple[float, float, float], self.read("fff")) - def write_rotation(self, x: float, y: float, z: float) -> Buffer: + def write_rotation(self, x: float, y: float, z: float) -> Self: """Writes a rotation to the buffer.""" return self.write("fff", x, y, z) @@ -287,7 +294,7 @@ def read_direction(self) -> Direction: return Direction(self.read_varint()) - def write_direction(self, value: Direction) -> Buffer: + def write_direction(self, value: Direction) -> Self: """Writes a direction to the buffer.""" return self.write_varint(value.value) @@ -297,24 +304,24 @@ def read_pose(self) -> Pose: return Pose(self.read_varint()) - def write_pose(self, value: Pose) -> Buffer: + def write_pose(self, value: Pose) -> Self: """Writes a pose to the buffer.""" return self.write_varint(value.value) - def write_recipe_item(self, value: Union[dict, str]) -> Buffer: + def write_recipe_item(self, value: Union[dict, str]) -> Self: """Writes a recipe item / slot to the buffer.""" if isinstance(value, dict): self.write_slot(**value) elif isinstance(value, str): - self.write_slot(value) + self.write_slot(value) # TODO: This function takes int, but we're passing str? else: raise TypeError(f"Invalid type {type(value)}.") return self - def write_ingredient(self, value: dict) -> Buffer: + def write_ingredient(self, value: dict) -> Self: """Writes a part of a recipe to the buffer.""" self.write_varint(len(value)) @@ -322,7 +329,9 @@ def write_ingredient(self, value: dict) -> Buffer: for slot in value.values(): self.write_recipe_item(slot) - def write_recipe(self, recipe_id: str, recipe: dict) -> Buffer: + return self + + def write_recipe(self, recipe_id: str, recipe: dict) -> Self: """Writes a recipe to the buffer.""" recipe_type = recipe["type"] @@ -380,7 +389,7 @@ def read_villager(self) -> dict: "level": self.read_varint(), } - def write_villager(self, kind: int, profession: int, level: int) -> Buffer: + def write_villager(self, kind: int, profession: int, level: int) -> Self: return self.write_varint(kind).write_varint(profession).write_varint(level) def write_trade( @@ -394,8 +403,8 @@ def write_trade( special_price: int, price_multi: float, demand: int, - in_item_2: dict = None, - ) -> Buffer: + in_item_2: Optional[dict] = None, + ) -> Self: self.write_slot(**in_item_1).write_slot(**out_item) if in_item_2 is not None: @@ -425,11 +434,11 @@ def read_particle(self) -> dict: particle["blue"] = self.read("f") particle["scale"] = self.read("f") elif particle_id == 32: - particle["item"] = self.read_slot() + particle["item"] = self.read_slot() # TODO: This function call is missing argument? return particle - def write_particle(self, **value) -> Buffer: + def write_particle(self, **value) -> Self: particle_id = value["particle_id"] if particle_id == 3 or particle_id == 23: @@ -441,9 +450,10 @@ def write_particle(self, **value) -> Buffer: return self - def write_entity_metadata(self, value: Dict[Tuple[int, int], object]) -> Buffer: + def write_entity_metadata(self, value: Dict[Tuple[int, int], object]) -> Self: def _f_10(v): """This is basically write_optional_position. + It's defined here because the function is too complex to be a lambda, so instead we just refer to this definition in the switch dict.""" self.write("?", v is not None) @@ -484,12 +494,12 @@ def _f_10(v): return self def read_modifier(self) -> Tuple[uuid.UUID, float, EntityModifier]: - return (self.read_uuid(), self.read("f"), EntityModifier(self.read("b"))) + return (self.read_uuid(), cast(float, self.read("f")), EntityModifier(self.read("b"))) def write_modifier(self, uuid_: uuid.UUID, amount: float, operation: EntityModifier): return self.write_uuid(uuid_).write("f", amount).write("b", operation) - def write_node(self, node: dict) -> Buffer: + def write_node(self, node: dict) -> Self: node_flags = node["flags"] self.write_byte(node_flags).write_varint(len(node["children"])) @@ -513,3 +523,9 @@ def write_node(self, node: dict) -> Buffer: self.write_string(node["suggestions_type"]) return self + + def write_block(self, value: object): + # TODO: This method is not yet implemented, but it needs to be here + # so that pyright doesn't mind us referencing it in other places + # but it should be implemented as soon as possible + raise NotImplementedError() diff --git a/pymine_net/types/nbt.py b/pymine_net/types/nbt.py index 5f81766..988d2cb 100644 --- a/pymine_net/types/nbt.py +++ b/pymine_net/types/nbt.py @@ -2,7 +2,7 @@ import gzip import struct -from typing import List +from typing import List, Optional, Type from mutf8 import decode_modified_utf8, encode_modified_utf8 @@ -25,7 +25,7 @@ "unpack", ) -TYPES: List[TAG] = [] +TYPES: List[Type[TAG]] = [] def unpack(buf, root_is_full: bool = True) -> TAG_Compound: @@ -76,7 +76,7 @@ class TAG: id = None - def __init__(self, name: str = None): + def __init__(self, name: Optional[str] = None): self.id = self.__class__.id self.name = "" if name is None else name @@ -113,10 +113,11 @@ def unpack(cls, buf) -> TAG: def pretty(self, indent: int = 0) -> str: return (" " * indent) + f'{self.__class__.__name__}("{self.name}"): {self.data}' - def __str__(self): + def __str__(self) -> str: return self.pretty() - __repr__ = __str__ + def __repr__(self) -> str: + return self.pretty() class TAG_End(TAG): @@ -153,7 +154,7 @@ class TAG_Byte(TAG): id = 1 - def __init__(self, name: str, data: int): + def __init__(self, name: Optional[str], data: int): super().__init__(name) self.data = data @@ -176,7 +177,7 @@ class TAG_Short(TAG): id = 2 - def __init__(self, name: str, data: int): + def __init__(self, name: Optional[str], data: int): super().__init__(name) self.data = data @@ -199,7 +200,7 @@ class TAG_Int(TAG): id = 3 - def __init__(self, name: str, data: int): + def __init__(self, name: Optional[str], data: int): super().__init__(name) self.data = data @@ -222,7 +223,7 @@ class TAG_Long(TAG): id = 4 - def __init__(self, name: str, data: int): + def __init__(self, name: Optional[str], data: int): super().__init__(name) self.data = data @@ -245,7 +246,7 @@ class TAG_Float(TAG): id = 5 - def __init__(self, name: str, data: float): + def __init__(self, name: Optional[str], data: float): super().__init__(name) self.data = data @@ -268,7 +269,7 @@ class TAG_Double(TAG): id = 6 - def __init__(self, name: str, data: float): + def __init__(self, name: Optional[str], data: float): super().__init__(name) self.data = data @@ -291,7 +292,7 @@ class TAG_Byte_Array(TAG, bytearray): id = 7 - def __init__(self, name: str, data: bytearray): + def __init__(self, name: Optional[str], data: bytearray): TAG.__init__(self, name) if isinstance(data, str): @@ -321,7 +322,7 @@ class TAG_String(TAG): id = 8 - def __init__(self, name: str, data: str): + def __init__(self, name: Optional[str], data: str): super().__init__(name) self.data = data @@ -348,7 +349,7 @@ class TAG_List(TAG, list): id = 9 - def __init__(self, name: str, data: List[TAG]): + def __init__(self, name: Optional[str], data: List[TAG]): TAG.__init__(self, name) list.__init__(self, data) @@ -386,7 +387,7 @@ class TAG_Compound(TAG, dict): id = 10 - def __init__(self, name: str, data: List[TAG]): + def __init__(self, name: Optional[str], data: List[TAG]): TAG.__init__(self, name) dict.__init__(self, [(t.name, t) for t in data]) @@ -438,7 +439,7 @@ class TAG_Int_Array(TAG, list): id = 11 - def __init__(self, name: str, data: list): + def __init__(self, name: Optional[str], data: list): TAG.__init__(self, name) list.__init__(self, data) @@ -467,7 +468,7 @@ class TAG_Long_Array(TAG, list): id = 12 - def __init__(self, name: str, data: List[int]): + def __init__(self, name: Optional[str], data: List[int]): TAG.__init__(self, name) list.__init__(self, data) diff --git a/pymine_net/types/packet.py b/pymine_net/types/packet.py index 16260dd..9415ae4 100644 --- a/pymine_net/types/packet.py +++ b/pymine_net/types/packet.py @@ -1,7 +1,7 @@ from __future__ import annotations from abc import abstractmethod -from typing import ClassVar, Optional +from typing import ClassVar from pymine_net.strict_abc import StrictABC, optionalabstractmethod from pymine_net.types.buffer import Buffer @@ -12,10 +12,10 @@ class Packet(StrictABC): """Base Packet class. - :cvar id: Packet identification number. Defaults to None. + :cvar id: Packet identification number. """ - id: ClassVar[Optional[int]] = None + id: ClassVar[int] class ServerBoundPacket(Packet): diff --git a/pymine_net/types/packet_map.py b/pymine_net/types/packet_map.py index d85e15b..83b9aab 100644 --- a/pymine_net/types/packet_map.py +++ b/pymine_net/types/packet_map.py @@ -1,10 +1,13 @@ from __future__ import annotations -from typing import Dict, List, Tuple, Type, Union +from typing import TYPE_CHECKING, Dict, List, Literal, Tuple, Type, Union, overload from pymine_net.enums import GameState, PacketDirection from pymine_net.errors import DuplicatePacketIdError -from pymine_net.types.packet import ClientBoundPacket, Packet, ServerBoundPacket +from pymine_net.types.packet import ClientBoundPacket, ServerBoundPacket + +if TYPE_CHECKING: + from typing_extensions import Self class StatePacketMap: @@ -24,13 +27,12 @@ def __init__( def from_list( cls, state: GameState, - packets: List[Type[Packet]], + packets: List[Union[Type[ServerBoundPacket], Type[ClientBoundPacket]]], *, check_duplicates: bool = False, - ) -> StatePacketMap: + ) -> Self: server_bound = {} client_bound = {} - for packet in packets: if issubclass(packet, ServerBoundPacket): if check_duplicates and packet.id in server_bound: @@ -44,6 +46,10 @@ def from_list( "unknown", state, packet.id, PacketDirection.CLIENTBOUND ) client_bound[packet.id] = packet + else: + raise TypeError( + f"Expected ServerBoundPacket or ClientBoundPacket, got {packet} ({type(packet)})" + ) return cls(state, server_bound, client_bound) @@ -55,8 +61,22 @@ def __init__(self, protocol: Union[str, int], packets: Dict[GameState, StatePack self.protocol = protocol self.packets = packets - def __getitem__(self, key: Tuple[PacketDirection, GameState, int]) -> Packet: - direction, state, packet_id = key + @overload + def __getitem__( + self, __key: Tuple[Literal[PacketDirection.CLIENTBOUND], GameState, int] + ) -> Type[ClientBoundPacket]: + ... + + @overload + def __getitem__( + self, __key: Tuple[Literal[PacketDirection.SERVERBOUND], GameState, int] + ) -> Type[ServerBoundPacket]: + ... + + def __getitem__( + self, __key: Tuple[PacketDirection, GameState, int] + ) -> Union[Type[ClientBoundPacket], Type[ServerBoundPacket]]: + direction, state, packet_id = __key if direction is PacketDirection.CLIENTBOUND: return self.packets[state].client_bound[packet_id] diff --git a/pymine_net/types/player.py b/pymine_net/types/player.py index a587259..83c66ba 100644 --- a/pymine_net/types/player.py +++ b/pymine_net/types/player.py @@ -2,13 +2,15 @@ import random import struct -from typing import Dict, List, Optional, Set +from typing import Dict, List, Optional, Set, TypeVar, Union from uuid import UUID import pymine_net.types.nbt as nbt from pymine_net.enums import ChatMode, GameMode, MainHand, SkinPart from pymine_net.types.vector import Rotation, Vector3 +T = TypeVar("T") + class PlayerProperty: __slots__ = ("name", "value", "signature") @@ -30,14 +32,14 @@ def __init__(self, entity_id: int, data: nbt.TAG_Compound): ] = data # typehinted as Dict[str, nbt.TAG] for ease of development # attributes like player settings not stored in Player._data - self.username: str = None + self.username: Optional[str] = None self.properties: List[PlayerProperty] = [] self.latency = -1 - self.display_name: str = None + self.display_name: Optional[str] = None # attributes from PlayClientSettings packet - self.locale: str = None - self.view_distance: int = None + self.locale: Optional[str] = None + self.view_distance: Optional[int] = None self.chat_mode: ChatMode = ChatMode.ENABLED self.chat_colors: bool = True self.displayed_skin_parts: Set[SkinPart] = set() @@ -58,7 +60,7 @@ def __setitem__(self, key: str, value: nbt.TAG) -> None: self._data[key] = value - def get(self, key: str, default: object = None) -> Optional[nbt.TAG]: + def get(self, key: str, default: T = None) -> Union[Optional[nbt.TAG], T]: """Gets an NBT tag from the internal NBT compound tag.""" try: diff --git a/pymine_net/types/registry.py b/pymine_net/types/registry.py index e8390c5..cbfcfd2 100644 --- a/pymine_net/types/registry.py +++ b/pymine_net/types/registry.py @@ -1,4 +1,7 @@ -from typing import Union +from typing import TYPE_CHECKING, Any, Mapping, Optional, Sequence + +if TYPE_CHECKING: + from typing_extensions import Self __all__ = ("Registry",) @@ -8,32 +11,46 @@ class Registry: def __init__( self, - data: Union[dict, list, tuple], - data_reversed: Union[dict, list, tuple] = None, + data: Mapping, + data_reversed: Optional[Mapping] = None, ): - self.data_reversed = data_reversed - - if isinstance(data, dict): - self.data = data - - if data_reversed is None: - self.data_reversed = {v: k for k, v in data.items()} - # When we get an iterable, we want to treat the positions as the - # IDs for the values. - elif isinstance(data, (list, tuple)): - self.data = {v: i for i, v in enumerate(data)} - self.data_reversed = data + """ + Makes a doubly hashed map, providing O(1) lookups for both keys and values. + + If data_reversed is specified, we use it for the reverse map instead of autogenerating + one from data by simply swapping keys and values. + """ + if data_reversed is not None and not isinstance(data_reversed, Mapping): + raise TypeError(f"data_reversed must be a Mapping, got {type(data_reversed)}.") + + if isinstance(data, Mapping): + self.data = dict(data) else: - raise TypeError( - "Creating a registry from something other than a dict, tuple, or list isn't supported" - ) + raise TypeError(f"Can't make registry from {type(data)}, must be Sequence/Mapping.") + + # Generate reverse mapping if it wasn't passed directly + if data_reversed is None: + self.data_reversed = {v: k for k, v in self.data.items()} + else: + self.data_reversed = data_reversed + + @classmethod + def from_sequence(cls, seq: Sequence, reversed_data: Optional[Mapping] = None) -> Self: + """ + Initialize the registry from a sequence, using it's positions (indices) + as the values (often useful with things like numeric block IDs, etc.) + and it's elements as the keys. + + If reversed_data is passed, we simply pass it over to __init__. + """ + return cls({v: i for i, v in enumerate(seq)}, reversed_data) - def encode(self, key: object) -> object: + def encode(self, key: Any) -> Any: """Key -> value, most likely an identifier to an integer.""" return self.data[key] - def decode(self, value: object) -> object: + def decode(self, value: Any) -> Any: """Value -> key, most likely a numeric id to a string identifier.""" return self.data_reversed[value] diff --git a/pyproject.toml b/pyproject.toml index 7fa1a2b..0e8bc2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,7 @@ pytest = "^7.0.1" colorama = "^0.4.4" isort = "^5.10.1" pytest-asyncio = "^0.18.1" +pyright = "^1.1.225" [build-system] requires = ["poetry-core>=1.0.0"]