Skip to content

Commit

Permalink
Enable reporting unknown types
Browse files Browse the repository at this point in the history
  • Loading branch information
ItsDrike committed Jul 13, 2024
1 parent 347dd59 commit e434651
Show file tree
Hide file tree
Showing 20 changed files with 93 additions and 61 deletions.
1 change: 1 addition & 0 deletions changes/333.internal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Enable reporting unknown types for basedpyright.
12 changes: 6 additions & 6 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
Expand All @@ -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]

Expand Down
18 changes: 9 additions & 9 deletions docs/extensions/attributetable.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
}
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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}
4 changes: 4 additions & 0 deletions mcproto/auth/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions mcproto/auth/microsoft/xbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from typing import NamedTuple

import httpx
from typing_extensions import override
from typing_extensions import Any, override

__all__ = [
"XSTSErrorType",
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion mcproto/auth/msa.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down
8 changes: 4 additions & 4 deletions mcproto/auth/yggdrasil.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}")

Expand Down Expand Up @@ -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},
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion mcproto/buffer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions mcproto/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()


Expand Down Expand Up @@ -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()


Expand Down Expand Up @@ -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]):
Expand Down
2 changes: 1 addition & 1 deletion mcproto/multiplayer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down
2 changes: 1 addition & 1 deletion mcproto/packets/packet.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
4 changes: 3 additions & 1 deletion mcproto/packets/packet_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down
28 changes: 25 additions & 3 deletions mcproto/types/nbt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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():
Expand All @@ -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]
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tests/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/mcproto/protocol/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand Down
Loading

0 comments on commit e434651

Please sign in to comment.