diff --git a/changes/333.internal.md b/changes/333.internal.md new file mode 100644 index 00000000..0d9ac837 --- /dev/null +++ b/changes/333.internal.md @@ -0,0 +1 @@ +Enable reporting unknown types for basedpyright. diff --git a/docs/conf.py b/docs/conf.py index 01f07683..91a23252 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,7 +14,7 @@ from pathlib import Path from packaging.version import parse as parse_version -from typing_extensions import override +from typing_extensions import Any, override if sys.version_info >= (3, 11): from tomllib import load as toml_parse @@ -110,7 +110,7 @@ autodoc_member_order = "bysource" # Default options for all autodoc directives -autodoc_default_options = { +autodoc_default_options: dict[str, Any] = { "members": True, "undoc-members": True, "show-inheritance": True, @@ -198,10 +198,10 @@ def override_towncrier_draft_format() -> None: from docutils import statemachine from sphinx.util.nodes import nodes - orig_f = sphinxcontrib.towncrier.ext._nodes_from_document_markup_source # pyright: ignore[reportPrivateUsage] + orig_f = sphinxcontrib.towncrier.ext._nodes_from_document_markup_source # pyright: ignore[reportPrivateUsage,reportUnknownMemberType,reportUnknownVariableType] def override_f( - state: statemachine.State, # pyright: ignore[reportMissingTypeArgument] # arg not specified in orig_f either + state: statemachine.State, # pyright: ignore[reportMissingTypeArgument,reportUnknownParameterType] # arg not specified in orig_f either markup_source: str, ) -> list[nodes.Node]: markup_source = markup_source.replace("## Version Unreleased changes", "## Unreleased changes") @@ -212,9 +212,9 @@ def override_f( markup_source = markup_source[:-3] markup_source = markup_source.rstrip(" \n") - markup_source = m2r2.M2R()(markup_source) + markup_source = m2r2.M2R()(markup_source) # pyright: ignore[reportUnknownVariableType] - return orig_f(state, markup_source) + return orig_f(state, markup_source) # pyright: ignore[reportUnknownArgumentType] sphinxcontrib.towncrier.ext._nodes_from_document_markup_source = override_f # pyright: ignore[reportPrivateUsage] diff --git a/docs/extensions/attributetable.py b/docs/extensions/attributetable.py index bbcb8b6e..a42f3ba1 100644 --- a/docs/extensions/attributetable.py +++ b/docs/extensions/attributetable.py @@ -55,7 +55,7 @@ def visit_attributetabletitle_node(self: HTML5Translator, node: AttributeTableTi def visit_attributetablebadge_node(self: HTML5Translator, node: AttributeTableBadge) -> None: - attributes = { + attributes: dict[str, Any] = { "class": "py-attribute-table-badge", "title": node["badge-type"], } @@ -153,7 +153,7 @@ def run(self) -> list[AttributeTablePlaceholder]: def build_lookup_table(env: BuildEnvironment) -> dict[str, list[str]]: # Given an environment, load up a lookup table of # full-class-name: objects - result = {} + result: dict[str, list[str]] = {} domain = env.domains["py"] ignored = { @@ -285,11 +285,11 @@ def class_results_to_node(key: str, elements: Sequence[TableElement]) -> Attribu def setup(app: Sphinx) -> dict[str, Any]: app.add_directive("attributetable", PyAttributeTable) - app.add_node(AttributeTable, html=(visit_attributetable_node, depart_attributetable_node)) - app.add_node(AttributeTableColumn, html=(visit_attributetablecolumn_node, depart_attributetablecolumn_node)) - app.add_node(AttributeTableTitle, html=(visit_attributetabletitle_node, depart_attributetabletitle_node)) - app.add_node(AttributeTableBadge, html=(visit_attributetablebadge_node, depart_attributetablebadge_node)) - app.add_node(AttributeTableItem, html=(visit_attributetable_item_node, depart_attributetable_item_node)) - app.add_node(AttributeTablePlaceholder) - _ = app.connect("doctree-resolved", process_attributetable) + app.add_node(AttributeTable, html=(visit_attributetable_node, depart_attributetable_node)) # pyright: ignore[reportUnknownMemberType] + app.add_node(AttributeTableColumn, html=(visit_attributetablecolumn_node, depart_attributetablecolumn_node)) # pyright: ignore[reportUnknownMemberType] + app.add_node(AttributeTableTitle, html=(visit_attributetabletitle_node, depart_attributetabletitle_node)) # pyright: ignore[reportUnknownMemberType] + app.add_node(AttributeTableBadge, html=(visit_attributetablebadge_node, depart_attributetablebadge_node)) # pyright: ignore[reportUnknownMemberType] + app.add_node(AttributeTableItem, html=(visit_attributetable_item_node, depart_attributetable_item_node)) # pyright: ignore[reportUnknownMemberType] + app.add_node(AttributeTablePlaceholder) # pyright: ignore[reportUnknownMemberType] + _ = app.connect("doctree-resolved", process_attributetable) # pyright: ignore[reportUnknownMemberType] return {"parallel_read_safe": True} diff --git a/mcproto/auth/account.py b/mcproto/auth/account.py index 57d8cfb5..39954154 100644 --- a/mcproto/auth/account.py +++ b/mcproto/auth/account.py @@ -45,6 +45,10 @@ class Account: __slots__ = ("access_token", "username", "uuid") + username: str + uuid: McUUID + access_token: str + def __init__(self, username: str, uuid: McUUID, access_token: str) -> None: self.username = username self.uuid = uuid diff --git a/mcproto/auth/microsoft/xbox.py b/mcproto/auth/microsoft/xbox.py index d454eb0a..e1da9818 100644 --- a/mcproto/auth/microsoft/xbox.py +++ b/mcproto/auth/microsoft/xbox.py @@ -4,7 +4,7 @@ from typing import NamedTuple import httpx -from typing_extensions import override +from typing_extensions import Any, override __all__ = [ "XSTSErrorType", @@ -66,7 +66,7 @@ def __init__(self, exc: httpx.HTTPStatusError): @property def msg(self) -> str: """Produce a message for this error.""" - msg_parts = [] + msg_parts: list[str] = [] if self.err_type is not XSTSErrorType.UNKNOWN: msg_parts.append(f"{self.err_type.name}: {self.err_type.value!r}") else: @@ -95,7 +95,7 @@ async def xbox_auth(client: httpx.AsyncClient, microsoft_access_token: str, bedr See :func:`~mcproto.auth.microsoft.oauth.full_microsoft_oauth` for info on ``microsoft_access_token``. """ # Obtain XBL token - payload = { + payload: dict[str, Any] = { "Properties": { "AuthMethod": "RPS", "SiteName": "user.auth.xboxlive.com", diff --git a/mcproto/auth/msa.py b/mcproto/auth/msa.py index 6a4bf621..183238f8 100644 --- a/mcproto/auth/msa.py +++ b/mcproto/auth/msa.py @@ -48,7 +48,7 @@ def __init__(self, exc: httpx.HTTPStatusError): @property def msg(self) -> str: """Produce a message for this error.""" - msg_parts = [] + msg_parts: list[str] = [] msg_parts.append(f"HTTP {self.code} from {self.url}:") msg_parts.append(f"type={self.err_type.name!r}") diff --git a/mcproto/auth/yggdrasil.py b/mcproto/auth/yggdrasil.py index d7428cc0..ce54c2b2 100644 --- a/mcproto/auth/yggdrasil.py +++ b/mcproto/auth/yggdrasil.py @@ -5,7 +5,7 @@ from uuid import uuid4 import httpx -from typing_extensions import Self, override +from typing_extensions import Any, Self, override from mcproto.auth.account import Account from mcproto.types.uuid import UUID as McUUID # noqa: N811 @@ -89,7 +89,7 @@ def __init__(self, exc: httpx.HTTPStatusError): @property def msg(self) -> str: """Produce a message for this error.""" - msg_parts = [] + msg_parts: list[str] = [] msg_parts.append(f"HTTP {self.code} from {self.url}:") msg_parts.append(f"type={self.err_type.name!r}") @@ -126,7 +126,7 @@ async def refresh(self, client: httpx.AsyncClient) -> None: having to go through a complete re-login. This can happen after some time period, or for example when someone else logs in to this minecraft account elsewhere. """ - payload = { + payload: dict[str, Any] = { "accessToken": self.access_token, "clientToken": self.client_token, "selectedProfile": {"id": str(self.uuid), "name": self.username}, @@ -196,7 +196,7 @@ async def authenticate(cls, client: httpx.AsyncClient, login: str, password: str # Any random string, we use a random v4 uuid, needs to remain same in further communications client_token = str(uuid4()) - payload = { + payload: dict[str, Any] = { "agent": { "name": "Minecraft", "version": 1, diff --git a/mcproto/buffer.py b/mcproto/buffer.py index e9c94adc..55c57d02 100644 --- a/mcproto/buffer.py +++ b/mcproto/buffer.py @@ -12,7 +12,7 @@ class Buffer(BaseSyncWriter, BaseSyncReader, bytearray): __slots__ = ("pos",) - def __init__(self, *args, **kwargs): + def __init__(self, *args: object, **kwargs: object): super().__init__(*args, **kwargs) self.pos = 0 diff --git a/mcproto/connection.py b/mcproto/connection.py index 7408d68f..9b6fe33e 100644 --- a/mcproto/connection.py +++ b/mcproto/connection.py @@ -143,7 +143,7 @@ def __enter__(self) -> Self: raise IOError("Connection already closed.") return self - def __exit__(self, *a, **kw) -> None: + def __exit__(self, *a: object, **kw: object) -> None: self.close() @@ -260,7 +260,7 @@ async def __aenter__(self) -> Self: raise IOError("Connection already closed.") return self - async def __aexit__(self, *a, **kw) -> None: + async def __aexit__(self, *a: object, **kw: object) -> None: await self.close() @@ -367,7 +367,7 @@ def socket(self) -> socket.socket: """Obtain the underlying socket behind the :class:`~asyncio.Transport`.""" # TODO: This should also have pyright: ignore[reportPrivateUsage] # See: https://github.com/DetachHead/basedpyright/issues/494 - return self.writer.transport._sock # pyright: ignore[reportAttributeAccessIssue] + return self.writer.transport._sock # pyright: ignore[reportAttributeAccessIssue,reportUnknownMemberType,reportUnknownVariableType] class UDPSyncConnection(SyncConnection, Generic[T_SOCK]): diff --git a/mcproto/multiplayer.py b/mcproto/multiplayer.py index 752f7d14..7d031e77 100644 --- a/mcproto/multiplayer.py +++ b/mcproto/multiplayer.py @@ -65,7 +65,7 @@ def __init__(self, exc: httpx.HTTPStatusError): @property def msg(self) -> str: """Produce a message for this error.""" - msg_parts = [] + msg_parts: list[str] = [] msg_parts.append(f"HTTP {self.code} from {self.url}:") msg_parts.append(f"type={self.err_type.name!r}") diff --git a/mcproto/packets/packet.py b/mcproto/packets/packet.py index 4f65a0b7..059687fa 100644 --- a/mcproto/packets/packet.py +++ b/mcproto/packets/packet.py @@ -128,7 +128,7 @@ def from_packet_class(cls, packet_class: type[Packet], buffer: Buffer, message: @property def msg(self) -> str: """Produce a message for this error.""" - msg_parts = [] + msg_parts: list[str] = [] if self.direction is PacketDirection.CLIENTBOUND: msg_parts.append("Clientbound") diff --git a/mcproto/packets/packet_map.py b/mcproto/packets/packet_map.py index aed3803b..4b2f40a2 100644 --- a/mcproto/packets/packet_map.py +++ b/mcproto/packets/packet_map.py @@ -4,7 +4,7 @@ import pkgutil from collections.abc import Iterator, Mapping, Sequence from types import MappingProxyType, ModuleType -from typing import Literal, NamedTuple, NoReturn, TYPE_CHECKING, overload +from typing import Literal, NamedTuple, NoReturn, TYPE_CHECKING, cast, overload from mcproto.packets.packet import ClientBoundPacket, GameState, Packet, PacketDirection, ServerBoundPacket @@ -61,10 +61,12 @@ def on_error(name: str) -> NoReturn: if not isinstance(member_names, Sequence): raise TypeError(f"Module {module_info.name!r}'s __all__ isn't defined as a sequence.") + member_names = cast(Sequence[object], member_names) for member_name in member_names: if not isinstance(member_name, str): raise TypeError(f"Module {module_info.name!r}'s __all__ contains non-string item.") + member_names = cast(Sequence[str], member_names) yield WalkableModuleData(imported_module, module_info, member_names) diff --git a/mcproto/types/nbt.py b/mcproto/types/nbt.py index 6fae7b25..87f5e5bb 100644 --- a/mcproto/types/nbt.py +++ b/mcproto/types/nbt.py @@ -330,32 +330,49 @@ def from_object(data: FromObjectType, schema: FromObjectSchema, name: str = "") :param name: The name of the NBT tag. :return: The NBT tag created from the python object. """ + # TODO: There are a lot of isinstance checks for dict/list, however, the FromObjectType/FromObjectSchema + # type alias declares a Sequence/Mapping type for these collections. The isinstance checks here should + # probably be replaced with these types (they should support runtime comparison). + + # TODO: Consider splitting this function up into smaller functions, that each parse a specific type of schema + # i.e. _from_object_dict, _from_object_list, _from_object_tag, ... + # Case 0 : schema is an object with a `to_nbt` method (could be a subclass of NBTag for all we know, as long # as the data is an instance of the schema it will work) if isinstance(schema, type) and hasattr(schema, "to_nbt") and isinstance(data, schema): return data.to_nbt(name=name) + # used later, declared explicitly since pyright can't infer this + # (recursive types aren't properly supported yet) + value: FromObjectType + # Case 1 : schema is a NBTag subclass if isinstance(schema, type) and issubclass(schema, NBTag): if schema in (CompoundNBT, ListNBT): raise ValueError("Use a list or a dictionary in the schema to create a CompoundNBT or a ListNBT.") + # Check if the data contains the name (if it is a dictionary) if isinstance(data, dict): + data = cast("dict[str, FromObjectType]", data) # recursive type, pyright can't infer if len(data) != 1: raise ValueError("Expected a dictionary with a single key-value pair.") # We also check if the name isn't already set if name: raise ValueError("The name is already set.") + key, value = next(iter(data.items())) # Recursive call to go to the next part return NBTag.from_object(value, schema, name=key) + # Else we check if the data can be a payload for the tag if not isinstance(data, (bytes, str, int, float, list)): raise TypeError(f"Expected one of (bytes, str, int, float, list), but found {type(data).__name__}.") + # Check if the data is a list of integers - if isinstance(data, list) and not all(isinstance(item, int) for item in data): + if isinstance(data, list) and not all(isinstance(item, int) for item in data): # pyright: ignore[reportUnknownVariableType] raise TypeError("Expected a list of integers, but a non-integer element was found.") data = cast(Union[bytes, str, int, float, "list[int]"], data) + # Create the tag with the data and the name return schema(data, name=name) # pyright: ignore[reportCallIssue] # The schema is a subclass of NBTag @@ -368,9 +385,11 @@ def from_object(data: FromObjectType, schema: FromObjectSchema, name: str = "") # Case 2 : schema is a dictionary payload: list[NBTag] = [] if isinstance(schema, dict): + schema = cast("dict[str, FromObjectSchema]", schema) # recursive type, pyright can't infer # We can unpack the dictionary and create a CompoundNBT tag if not isinstance(data, dict): raise TypeError(f"Expected a dictionary, but found a different type ({type(data).__name__}).") + data = cast("dict[str, FromObjectType]", data) # recursive type, pyright can't infer # Iterate over the dictionary for key, value in data.items(): @@ -383,8 +402,11 @@ def from_object(data: FromObjectType, schema: FromObjectSchema, name: str = "") # We need to check if every element in the schema has the same type # but keep in mind that dict and list are also valid types, as long # as there are only dicts, or only lists in the schema + schema = cast("Sequence[FromObjectSchema]", schema) # recursive type, pyright can't infer if not isinstance(data, list): raise TypeError(f"Expected a list, but found {type(data).__name__}.") + data = cast("list[FromObjectType]", data) # recursive type, pyright can't infer + if len(schema) == 1: # We have two cases here, either the schema supports an unknown number of elements of a single type ... children_schema = schema[0] @@ -402,7 +424,7 @@ def from_object(data: FromObjectType, schema: FromObjectSchema, name: str = "") # Check that the schema only has one type of elements first_schema = schema[0] # Dict/List case - if isinstance(first_schema, (list, dict)) and not all(isinstance(item, type(first_schema)) for item in schema): + if isinstance(first_schema, (list, dict)) and not all(isinstance(item, type(first_schema)) for item in schema): # pyright: ignore[reportUnknownArgumentType] raise TypeError(f"Expected a list of lists or dictionaries, but found a different type ({schema=}).") # NBTag case # Ignore branch coverage, `schema` will never be an empty list here @@ -875,7 +897,7 @@ def to_object( 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): # pragma: no cover + if not all(isinstance(schema, type(first)) for schema in subschemas): # pyright: ignore[reportUnknownArgumentType] # pragma: no cover raise TypeError(f"All items in the list must have the same type. Found {subschemas!r}") return result, subschemas return result diff --git a/pyproject.toml b/pyproject.toml index 1a4560d7..7d4126e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,12 +99,8 @@ disableBytesTypePromotions = true reportAny = false reportImplicitStringConcatenation = false reportUnreachable = "information" -reportUnknownArgumentType = false # consider enabling -reportUnknownVariableType = false # consider enabling -reportUnknownMemberType = false # consider enabling -reportUnknownParameterType = false # consider enabling -reportUnknownLambdaType = false # consider enabling reportMissingTypeStubs = "information" # consider bumping to warning/error +reportUnknownLambdaType = false # consider enabling reportUninitializedInstanceVariable = false # until https://github.com/DetachHead/basedpyright/issues/491 reportMissingParameterType = false # ruff's flake8-annotations (ANN) already covers this + gives us more control diff --git a/tests/helpers.py b/tests/helpers.py index 69f7a639..c3051f97 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -141,7 +141,7 @@ class be of/return the same class. _mock_sealed: bool _extract_mock_name: Callable[[], str] - def _get_child_mock(self, **kwargs) -> T_Mock: + def _get_child_mock(self, **kwargs: object) -> T_Mock: """Make :attr:`.child_mock_type`` instances instead of instances of the same class. By default, this method creates a new mock instance of the same original class, and passes diff --git a/tests/mcproto/protocol/helpers.py b/tests/mcproto/protocol/helpers.py index 569ecf99..f530ad17 100644 --- a/tests/mcproto/protocol/helpers.py +++ b/tests/mcproto/protocol/helpers.py @@ -8,7 +8,7 @@ class WriteFunctionMock(Mock): """Mock write function, storing the written data.""" - def __init__(self, *a, **kw): + def __init__(self, *a: object, **kw: object): super().__init__(*a, **kw) self.combined_data = bytearray() @@ -40,7 +40,7 @@ class WriteFunctionAsyncMock(WriteFunctionMock, AsyncMock): # pyright: ignore[r class ReadFunctionMock(Mock): """Mock read function, giving pre-defined data.""" - def __init__(self, *a, combined_data: bytearray | None = None, **kw): + def __init__(self, *a: object, combined_data: bytearray | None = None, **kw: object): super().__init__(*a, **kw) if combined_data is None: combined_data = bytearray() diff --git a/tests/mcproto/test_connection.py b/tests/mcproto/test_connection.py index 3054513f..fd47804a 100644 --- a/tests/mcproto/test_connection.py +++ b/tests/mcproto/test_connection.py @@ -22,7 +22,7 @@ class MockSocket(CustomMockMixin[MagicMock], MagicMock): # pyright: ignore[repo spec_set = socket.socket - def __init__(self, *args, read_data: bytearray | None = None, **kwargs) -> None: + def __init__(self, *args: object, read_data: bytearray | None = None, **kwargs: object) -> None: super().__init__(*args, **kwargs) self.mock_add_spec(["_recv", "_send", "_closed"]) self._recv = ReadFunctionMock(combined_data=read_data) @@ -60,7 +60,7 @@ class MockStreamWriter(CustomMockMixin[MagicMock], MagicMock): # pyright: ignor spec_set = asyncio.StreamWriter - def __init__(self, *args, **kwargs): + def __init__(self, *args: object, **kwargs: object): super().__init__(*args, **kwargs) self.mock_add_spec(["_white", "_closed"]) self._write = WriteFunctionMock() @@ -86,7 +86,7 @@ class MockStreamReader(CustomMockMixin[MagicMock], MagicMock): # pyright: ignor spec_set = asyncio.StreamReader - def __init__(self, *args, read_data: bytearray | None = None, **kwargs) -> None: + def __init__(self, *args: object, read_data: bytearray | None = None, **kwargs: object) -> None: super().__init__(*args, **kwargs) self.mock_add_spec(["_read"]) self._read = ReadFunctionAsyncMock(combined_data=read_data) diff --git a/tests/mcproto/test_multiplayer.py b/tests/mcproto/test_multiplayer.py index 5ce51950..eeab3c2b 100644 --- a/tests/mcproto/test_multiplayer.py +++ b/tests/mcproto/test_multiplayer.py @@ -89,7 +89,7 @@ async def test_join_request_invalid( ("172.17.0.1"), ], ) -async def test_join_check_valid(client_ip, httpx_mock: HTTPXMock): +async def test_join_check_valid(client_ip: str | None, httpx_mock: HTTPXMock): """Test making a join check, getting back a valid response.""" client_username = "ItsDrike" server_hash = "-745fc7fdb2d6ae7c4b20e2987770def8f3dd1105" diff --git a/tests/mcproto/types/test_chat.py b/tests/mcproto/types/test_chat.py index 54f16583..e5c23d13 100644 --- a/tests/mcproto/types/test_chat.py +++ b/tests/mcproto/types/test_chat.py @@ -1,5 +1,7 @@ from __future__ import annotations +from typing import cast + import pytest from mcproto.types.chat import ChatMessage, RawChatMessage, RawChatMessageDict @@ -8,20 +10,23 @@ @pytest.mark.parametrize( ("raw", "expected_dict"), - [ - ( - {"text": "A Minecraft Server"}, - {"text": "A Minecraft Server"}, - ), - ( - "A Minecraft Server", - {"text": "A Minecraft Server"}, - ), - ( - [{"text": "hello", "bold": True}, {"text": "there"}], - {"extra": [{"text": "hello", "bold": True}, {"text": "there"}]}, - ), - ], + cast( + "list[tuple[RawChatMessage, RawChatMessageDict]]", + [ + ( + {"text": "A Minecraft Server"}, + {"text": "A Minecraft Server"}, + ), + ( + "A Minecraft Server", + {"text": "A Minecraft Server"}, + ), + ( + [{"text": "hello", "bold": True}, {"text": "there"}], + {"extra": [{"text": "hello", "bold": True}, {"text": "there"}]}, + ), + ], + ), ) def test_as_dict(raw: RawChatMessage, expected_dict: RawChatMessageDict): """Test converting raw ChatMessage input into dict produces expected dict.""" diff --git a/tests/mcproto/types/test_nbt.py b/tests/mcproto/types/test_nbt.py index fecd067b..cdccf138 100644 --- a/tests/mcproto/types/test_nbt.py +++ b/tests/mcproto/types/test_nbt.py @@ -13,6 +13,8 @@ DoubleNBT, EndNBT, FloatNBT, + FromObjectSchema, + FromObjectType, IntArrayNBT, IntNBT, ListNBT, @@ -477,7 +479,7 @@ def test_nbt_bigfile(): data = bytes.fromhex(data) buffer = Buffer(data) - expected_object = { # Name ! Level + expected_object: FromObjectType = { # Name ! Level "longTest": 9223372036854775807, "shortTest": 32767, "stringTest": "HELLO WORLD THIS IS A TEST STRING ÅÄÖ!", @@ -498,7 +500,7 @@ def test_nbt_bigfile(): "starting with n=0 (0, 62, 34, 16, 8, ...))": bytes((n * n * 255 + n * 7) % 100 for n in range(1000)), "doubleTest": 0.4921875, } - expected_schema = { + expected_schema: FromObjectSchema = { "longTest": LongNBT, "shortTest": ShortNBT, "stringTest": StringNBT,