From 92663be9ae3711864270981f34c4a64c9b5f9bcd Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 16 Jul 2023 01:08:51 +0200 Subject: [PATCH 01/11] Enforce RUF lint rules Ruff has it's own custom set of linter rules, this enables all of them. --- docs/extensions/attributetable.py | 2 +- pyproject.toml | 2 +- tests/mcproto/protocol/test_base_io.py | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/extensions/attributetable.py b/docs/extensions/attributetable.py index 8ca1d035..7d4601da 100644 --- a/docs/extensions/attributetable.py +++ b/docs/extensions/attributetable.py @@ -93,7 +93,7 @@ class PyAttributeTable(SphinxDirective): required_arguments = 1 optional_arguments = 0 final_argument_whitespace = False - option_spec: OptionSpec = {} + option_spec: OptionSpec = {} # noqa: RUF012 # (from original impl) def parse_name(self, content: str) -> tuple[str, str]: match = _name_parser_regex.match(content) diff --git a/pyproject.toml b/pyproject.toml index 4e487d3b..75207831 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,7 +115,7 @@ select = [ "RSE", # flake8-raise "SIM", # flake8-simplify "TID", # flake8-tidy-imports - "RUF100", # flake8-noqa + "RUF", # ruff-specific rules "UP006", # flake8-pep585 ] diff --git a/tests/mcproto/protocol/test_base_io.py b/tests/mcproto/protocol/test_base_io.py index 52cd2bac..57e9a36c 100644 --- a/tests/mcproto/protocol/test_base_io.py +++ b/tests/mcproto/protocol/test_base_io.py @@ -316,8 +316,8 @@ def test_write_bytearray(self, data: bytes, expected_bytes: list[int], write_moc @pytest.mark.parametrize( ("string", "expected_bytes"), [ - ("test", list(map(ord, "test")) + [0]), - ("a" * 100, list(map(ord, "a" * 100)) + [0]), + ("test", [*list(map(ord, "test")), 0]), + ("a" * 100, [*list(map(ord, "a" * 100)), 0]), ("", [0]), ], ) @@ -329,8 +329,8 @@ def test_write_ascii(self, string: str, expected_bytes: list[int], write_mock: W @pytest.mark.parametrize( ("string", "expected_bytes"), [ - ("test", [len("test")] + list(map(ord, "test"))), - ("a" * 100, [len("a" * 100)] + list(map(ord, "a" * 100))), + ("test", [len("test"), *list(map(ord, "test"))]), + ("a" * 100, [len("a" * 100), *list(map(ord, "a" * 100))]), ("", [0]), ("नमस्ते", [18] + [int(x) for x in "नमस्ते".encode("utf-8")]), ], @@ -507,8 +507,8 @@ def test_read_bytearray(self, read_bytes: list[int], expected_bytes: bytes, read @pytest.mark.parametrize( ("read_bytes", "expected_string"), [ - (list(map(ord, "test")) + [0], "test"), - (list(map(ord, "a" * 100)) + [0], "a" * 100), + ([*list(map(ord, "test")), 0], "test"), + ([*list(map(ord, "a" * 100)), 0], "a" * 100), ([0], ""), ], ) @@ -520,8 +520,8 @@ def test_read_ascii(self, read_bytes: list[int], expected_string: str, read_mock @pytest.mark.parametrize( ("read_bytes", "expected_string"), [ - ([len("test")] + list(map(ord, "test")), "test"), - ([len("a" * 100)] + list(map(ord, "a" * 100)), "a" * 100), + ([len("test"), *list(map(ord, "test"))], "test"), + ([len("a" * 100), *list(map(ord, "a" * 100))], "a" * 100), ([0], ""), ([18] + [int(x) for x in "नमस्ते".encode("utf-8")], "नमस्ते"), ], @@ -536,7 +536,7 @@ def test_read_utf(self, read_bytes: list[int], expected_string: str, read_mock: ("read_bytes"), [ [253, 255, 7], - [128, 128, 2] + list(map(ord, "a" * (32768))), + [128, 128, 2, *list(map(ord, "a" * 32768))], ], # Temporary workaround. # https://github.com/pytest-dev/pytest/issues/6881#issuecomment-596381626 From 5d6910f8109437d89c62445f43584a4063979d37 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 16 Jul 2023 01:10:37 +0200 Subject: [PATCH 02/11] Enable PTH linter rule This enables all ruff's flake8-use-pathlib (PTH) extension rules. --- docs/conf.py | 2 +- pyproject.toml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 03244664..d34f8da5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,7 +23,7 @@ # -- Basic project information ----------------------------------------------- -with open("../pyproject.toml", "rb") as f: +with Path("../pyproject.toml").open("rb") as f: pkg_meta: dict[str, str] = toml_parse(f)["tool"]["poetry"] project = str(pkg_meta["name"]) diff --git a/pyproject.toml b/pyproject.toml index 75207831..d3ddc3e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,6 +115,7 @@ select = [ "RSE", # flake8-raise "SIM", # flake8-simplify "TID", # flake8-tidy-imports + "PTH", # flake8-use-pathlib "RUF", # ruff-specific rules "UP006", # flake8-pep585 ] From 57a9769e089696c5c3ceea92ca0d0281b7cd0036 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 16 Jul 2023 01:11:37 +0200 Subject: [PATCH 03/11] Enable ASYNC linter rules This enables all ruff's flake8-async (ASYNC) rules. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index d3ddc3e9..806b28f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,6 +105,7 @@ select = [ "N", # pep8-naming "YTT", # flake8-2020 "ANN", # flake8-annotations + "ASYNC", # flake8-async "S", # flake8-bandit "B", # flake8-bugbear "A", # flake8-builtins From 93719baabc412ac944b26a02005a3894b0b7191c Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 16 Jul 2023 01:12:54 +0200 Subject: [PATCH 04/11] Enable Q linter rules This enables all ruff's flake8-quotes (Q) rules. --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 806b28f6..97d0da4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,6 +113,7 @@ select = [ "FA", # flake8-future-annotations "T20", # flake8-print "PT", # flake8-pytest-style + "Q", # flake8-quotes "RSE", # flake8-raise "SIM", # flake8-simplify "TID", # flake8-tidy-imports From ed2c9cb80e96219eeffa584f0b9ed4f850c536bc Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 16 Jul 2023 01:25:52 +0200 Subject: [PATCH 05/11] Enable RET linter rules This enables all ruff's flake8-return (RET) rules. --- mcproto/packets/interactions.py | 3 +-- mcproto/protocol/base_io.py | 12 ++++-------- mcproto/types/chat.py | 8 ++++---- pyproject.toml | 1 + 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/mcproto/packets/interactions.py b/mcproto/packets/interactions.py index d0ab6900..33013ab1 100644 --- a/mcproto/packets/interactions.py +++ b/mcproto/packets/interactions.py @@ -43,8 +43,7 @@ def _serialize_packet(packet: Packet, *, compressed: bool = False) -> Buffer: data_buf.write_varint(data_length) data_buf.write(packet_buf) return data_buf - else: - return packet_buf + return packet_buf def _deserialize_packet( diff --git a/mcproto/protocol/base_io.py b/mcproto/protocol/base_io.py index 10ebc427..f31b1d98 100644 --- a/mcproto/protocol/base_io.py +++ b/mcproto/protocol/base_io.py @@ -391,8 +391,7 @@ async def read_varint(self) -> int: For more information about variable length format check :meth:`._read_varuint`. """ unsigned_num = await self._read_varuint(max_bits=32) - val = from_twos_complement(unsigned_num, bits=32) - return val + return from_twos_complement(unsigned_num, bits=32) async def read_varlong(self) -> int: """Read a 64-bit signed integer in a variable length format. @@ -400,8 +399,7 @@ async def read_varlong(self) -> int: For more information about variable length format check :meth:`._read_varuint`. """ unsigned_num = await self._read_varuint(max_bits=64) - val = from_twos_complement(unsigned_num, bits=64) - return val + return from_twos_complement(unsigned_num, bits=64) async def read_bytearray(self, /) -> bytearray: """Read an arbitrary sequence of bytes, prefixed with a varint of it's size.""" @@ -533,8 +531,7 @@ def read_varint(self) -> int: For more information about variable length format check :meth:`._read_varuint`. """ unsigned_num = self._read_varuint(max_bits=32) - val = from_twos_complement(unsigned_num, bits=32) - return val + return from_twos_complement(unsigned_num, bits=32) def read_varlong(self) -> int: """Read a 64-bit signed integer in a variable length format. @@ -542,8 +539,7 @@ def read_varlong(self) -> int: For more information about variable length format check :meth:`._read_varuint`. """ unsigned_num = self._read_varuint(max_bits=64) - val = from_twos_complement(unsigned_num, bits=64) - return val + return from_twos_complement(unsigned_num, bits=64) def read_bytearray(self) -> bytearray: """Read an arbitrary sequence of bytes, prefixed with a varint of it's size.""" diff --git a/mcproto/types/chat.py b/mcproto/types/chat.py index 7c7db134..354a2341 100644 --- a/mcproto/types/chat.py +++ b/mcproto/types/chat.py @@ -42,12 +42,12 @@ def as_dict(self) -> RawChatMessageDict: """Convert received ``raw`` into a stadard :class:`dict` form.""" if isinstance(self.raw, list): return RawChatMessageDict(extra=self.raw) - elif isinstance(self.raw, str): + if isinstance(self.raw, str): return RawChatMessageDict(text=self.raw) - elif isinstance(self.raw, dict): # pyright: ignore[reportUnnecessaryIsInstance] + if isinstance(self.raw, dict): # pyright: ignore[reportUnnecessaryIsInstance] return self.raw - else: # pragma: no cover - raise TypeError(f"Found unexpected type ({self.raw.__class__!r}) ({self.raw!r}) in `raw` attribute") + # pragma: no cover + raise TypeError(f"Found unexpected type ({self.raw.__class__!r}) ({self.raw!r}) in `raw` attribute") def __eq__(self, other: Self) -> bool: """Check equality between two chat messages. diff --git a/pyproject.toml b/pyproject.toml index 97d0da4d..5fabddea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,6 +115,7 @@ select = [ "PT", # flake8-pytest-style "Q", # flake8-quotes "RSE", # flake8-raise + "RET", # flake8-return "SIM", # flake8-simplify "TID", # flake8-tidy-imports "PTH", # flake8-use-pathlib From 8f76213e94c70eb5a4e9a6fe1a5bf8da3b9eb7cd Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 16 Jul 2023 01:30:34 +0200 Subject: [PATCH 06/11] Enable UP linter rules This enables all ruff's pyupgrade (UP) rules. However UP024 (using errors that alias OSError) is ignored, as that prohibits use of exceptions like `IOError`, which is utilized pretty extensively in mcproto. --- mcproto/connection.py | 6 +++--- mcproto/packets/handshaking/handshake.py | 4 ++-- mcproto/packets/login/login.py | 4 ++-- mcproto/packets/packet_map.py | 2 +- mcproto/protocol/base_io.py | 18 ++++++++-------- mcproto/utils/deprecation.py | 16 +++++++-------- pyproject.toml | 3 ++- tests/mcproto/protocol/helpers.py | 3 +-- tests/mcproto/protocol/test_base_io.py | 26 ++++++++++++------------ tests/mcproto/test_connection.py | 9 ++++---- 10 files changed, 45 insertions(+), 46 deletions(-) diff --git a/mcproto/connection.py b/mcproto/connection.py index 5a949704..e24f65bd 100644 --- a/mcproto/connection.py +++ b/mcproto/connection.py @@ -4,7 +4,7 @@ import errno import socket from abc import ABC, abstractmethod -from typing import Generic, Optional, TypeVar +from typing import Generic, TypeVar import asyncio_dgram from typing_extensions import ParamSpec, Self @@ -262,7 +262,7 @@ def make_client(cls, address: tuple[str, int], timeout: float) -> Self: sock.settimeout(timeout) return cls(sock, address) - def read(self, length: Optional[int] = None) -> bytearray: + def read(self, length: int | None = None) -> bytearray: """Receive data sent through the connection. :param length: @@ -311,7 +311,7 @@ async def make_client(cls, address: tuple[str, int], timeout: float) -> Self: stream = await asyncio.wait_for(conn, timeout=timeout) return cls(stream, timeout) - async def read(self, length: Optional[int] = None) -> bytearray: + async def read(self, length: int | None = None) -> bytearray: """Receive data sent through the connection. :param length: diff --git a/mcproto/packets/handshaking/handshake.py b/mcproto/packets/handshaking/handshake.py index fd64058a..d9db1021 100644 --- a/mcproto/packets/handshaking/handshake.py +++ b/mcproto/packets/handshaking/handshake.py @@ -1,7 +1,7 @@ from __future__ import annotations from enum import IntEnum -from typing import ClassVar, Union, final +from typing import ClassVar, final from typing_extensions import Self @@ -35,7 +35,7 @@ def __init__( protocol_version: int, server_address: str, server_port: int, - next_state: Union[NextState, int], + next_state: NextState | int, ): """ :param protocol_version: Protocol version number to be used. diff --git a/mcproto/packets/login/login.py b/mcproto/packets/login/login.py index 9d8bdc06..7e317d1b 100644 --- a/mcproto/packets/login/login.py +++ b/mcproto/packets/login/login.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import ClassVar, Optional, final +from typing import ClassVar, final from typing_extensions import Self @@ -215,7 +215,7 @@ class LoginPluginResponse(ServerBoundPacket): PACKET_ID: ClassVar[int] = 0x02 GAME_STATE: ClassVar[GameState] = GameState.LOGIN - def __init__(self, message_id: int, data: Optional[bytes]): + def __init__(self, message_id: int, data: bytes | None): """ :param message_id: Message id, generated by the server, should be unique to the connection. :param data: Optional response data, present if client understood request. diff --git a/mcproto/packets/packet_map.py b/mcproto/packets/packet_map.py index ada5083a..f691220a 100644 --- a/mcproto/packets/packet_map.py +++ b/mcproto/packets/packet_map.py @@ -94,7 +94,7 @@ def generate_packet_map( ... -@lru_cache() +@lru_cache def generate_packet_map(direction: PacketDirection, state: GameState) -> Mapping[int, type[Packet]]: """Dynamically generated a packet map for given ``direction`` and ``state``. diff --git a/mcproto/protocol/base_io.py b/mcproto/protocol/base_io.py index f31b1d98..f89d1390 100644 --- a/mcproto/protocol/base_io.py +++ b/mcproto/protocol/base_io.py @@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable from enum import Enum from itertools import count -from typing import Literal, Optional, TypeVar, Union, overload +from typing import Literal, TypeVar, Union, overload from typing_extensions import TypeAlias @@ -105,7 +105,7 @@ async def write_value(self, fmt: StructFormat, value: object, /) -> None: """Write a given ``value`` as given struct format (``fmt``) in big-endian mode.""" await self.write(struct.pack(">" + fmt.value, value)) - async def _write_varuint(self, value: int, /, *, max_bits: Optional[int] = None) -> None: + async def _write_varuint(self, value: int, /, *, max_bits: int | None = None) -> None: """Write an arbitrarily big unsigned integer in a variable length format. This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes. @@ -179,7 +179,7 @@ async def write_utf(self, value: str, /) -> None: await self.write_varint(len(data)) await self.write(data) - async def write_optional(self, value: Optional[T], /, writer: Callable[[T], Awaitable[R]]) -> Optional[R]: + async def write_optional(self, value: T | None, /, writer: Callable[[T], Awaitable[R]]) -> R | None: """Writes a bool showing if a ``value`` is present, if so, also writes this value with ``writer`` function. * When ``value`` is ``None``, a bool of ``False`` will be written, and ``None`` is returned. @@ -223,7 +223,7 @@ def write_value(self, fmt: StructFormat, value: object, /) -> None: """Write a given ``value`` as given struct format (``fmt``) in big-endian mode.""" self.write(struct.pack(">" + fmt.value, value)) - def _write_varuint(self, value: int, /, *, max_bits: Optional[int] = None) -> None: + def _write_varuint(self, value: int, /, *, max_bits: int | None = None) -> None: """Write an arbitrarily big unsigned integer in a variable length format. This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes. @@ -297,7 +297,7 @@ def write_utf(self, value: str, /) -> None: self.write_varint(len(data)) self.write(data) - def write_optional(self, value: Optional[T], /, writer: Callable[[T], R]) -> Optional[R]: + def write_optional(self, value: T | None, /, writer: Callable[[T], R]) -> R | None: """Writes a bool showing if a ``value`` is present, if so, also writes this value with ``writer`` function. * When ``value`` is ``None``, a bool of ``False`` will be written, and ``None`` is returned. @@ -351,7 +351,7 @@ async def read_value(self, fmt: StructFormat, /) -> object: unpacked = struct.unpack(">" + fmt.value, data) return unpacked[0] - async def _read_varuint(self, *, max_bits: Optional[int] = None) -> int: + async def _read_varuint(self, *, max_bits: int | None = None) -> int: """Read an arbitrarily big unsigned integer in a variable length format. This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes. @@ -444,7 +444,7 @@ async def read_utf(self) -> str: return chars - async def read_optional(self, reader: Callable[[], Awaitable[R]]) -> Optional[R]: + async def read_optional(self, reader: Callable[[], Awaitable[R]]) -> R | None: """Reads a bool showing if a value is present, if so, also reads this value with ``reader`` function. * When ``False`` is read, the function will not read anything and ``None`` is returned. @@ -491,7 +491,7 @@ def read_value(self, fmt: StructFormat, /) -> object: unpacked = struct.unpack(">" + fmt.value, data) return unpacked[0] - def _read_varuint(self, *, max_bits: Optional[int] = None) -> int: + def _read_varuint(self, *, max_bits: int | None = None) -> int: """Read an arbitrarily big unsigned integer in a variable length format. This is a standard way of transmitting ints, and it allows smaller numbers to take less bytes. @@ -584,7 +584,7 @@ def read_utf(self) -> str: return chars - def read_optional(self, reader: Callable[[], R]) -> Optional[R]: + def read_optional(self, reader: Callable[[], R]) -> R | None: """Reads a bool showing if a value is present, if so, also reads this value with ``reader`` function. * When ``False`` is read, the function will not read anything and ``None`` is returned. diff --git a/mcproto/utils/deprecation.py b/mcproto/utils/deprecation.py index 56694f03..7bf4032d 100644 --- a/mcproto/utils/deprecation.py +++ b/mcproto/utils/deprecation.py @@ -4,7 +4,7 @@ import warnings from collections.abc import Callable from functools import wraps -from typing import Optional, TypeVar, Union +from typing import TypeVar from semantic_version import Version from typing_extensions import ParamSpec, Protocol @@ -18,9 +18,9 @@ def deprecation_warn( *, obj_name: str, - removal_version: Union[str, Version], - replacement: Optional[str] = None, - extra_msg: Optional[str] = None, + removal_version: str | Version, + replacement: str | None = None, + extra_msg: str | None = None, stack_level: int = 3, ) -> None: """Produce an appropriate deprecation warning given the parameters. @@ -76,10 +76,10 @@ def __call__(self, __x: Callable[P, R]) -> Callable[P, R]: def deprecated( - removal_version: Union[str, Version], - display_name: Optional[str] = None, - replacement: Optional[str] = None, - extra_msg: Optional[str] = None, + removal_version: str | Version, + display_name: str | None = None, + replacement: str | None = None, + extra_msg: str | None = None, ) -> DecoratorFunction: """Mark an object as deprecated. diff --git a/pyproject.toml b/pyproject.toml index 5fabddea..63236f76 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -120,7 +120,7 @@ select = [ "TID", # flake8-tidy-imports "PTH", # flake8-use-pathlib "RUF", # ruff-specific rules - "UP006", # flake8-pep585 + "UP", # pyupgrade ] ignore = [ @@ -130,6 +130,7 @@ ignore = [ "ANN102", # Missing type annotation for cls in classmethod "ANN204", # Missing return type annotation for special method "PT011", # pytest.raises without match parameter is too broad + "UP024", # Using errors that alias OSError ] [tool.ruff.extend-per-file-ignores] diff --git a/tests/mcproto/protocol/helpers.py b/tests/mcproto/protocol/helpers.py index a6aee118..d3381c46 100644 --- a/tests/mcproto/protocol/helpers.py +++ b/tests/mcproto/protocol/helpers.py @@ -1,6 +1,5 @@ from __future__ import annotations -from typing import Optional from unittest.mock import AsyncMock, Mock @@ -33,7 +32,7 @@ class WriteFunctionAsyncMock(WriteFunctionMock, AsyncMock): class ReadFunctionMock(Mock): - def __init__(self, *a, combined_data: Optional[bytearray] = None, **kw): + def __init__(self, *a, combined_data: bytearray | None = None, **kw): super().__init__(*a, **kw) if combined_data is None: combined_data = bytearray() diff --git a/tests/mcproto/protocol/test_base_io.py b/tests/mcproto/protocol/test_base_io.py index 57e9a36c..5f37825d 100644 --- a/tests/mcproto/protocol/test_base_io.py +++ b/tests/mcproto/protocol/test_base_io.py @@ -3,7 +3,7 @@ import platform import struct from abc import ABC, abstractmethod -from typing import Any, Union +from typing import Any from unittest.mock import AsyncMock, Mock import pytest @@ -159,7 +159,7 @@ def __init__(self): class WriterTests(ABC): """This class holds tests for both sync and async versions of writer.""" - writer: Union[BaseSyncWriter, BaseAsyncWriter] + writer: BaseSyncWriter | BaseAsyncWriter @classmethod @abstractmethod @@ -168,7 +168,7 @@ def setup_class(cls): ... @pytest.fixture() - def method_mock(self) -> Union[Mock, AsyncMock]: + def method_mock(self) -> Mock | AsyncMock: """Returns the appropriate type of mock, supporting both sync and async modes.""" if isinstance(self.writer, BaseSyncWriter): return Mock @@ -188,7 +188,7 @@ def autopatch(self, monkeypatch: pytest.MonkeyPatch): patch_path = "mcproto.protocol.base_io.BaseAsyncWriter" mock_type = AsyncMock - def autopatch(function_name: str) -> Union[Mock, AsyncMock]: + def autopatch(function_name: str) -> Mock | AsyncMock: mock_f = mock_type() monkeypatch.setattr(f"{patch_path}.{function_name}", mock_f) return mock_f @@ -332,7 +332,7 @@ def test_write_ascii(self, string: str, expected_bytes: list[int], write_mock: W ("test", [len("test"), *list(map(ord, "test"))]), ("a" * 100, [len("a" * 100), *list(map(ord, "a" * 100))]), ("", [0]), - ("नमस्ते", [18] + [int(x) for x in "नमस्ते".encode("utf-8")]), + ("नमस्ते", [18] + [int(x) for x in "नमस्ते".encode()]), ], ) def test_write_utf(self, string: str, expected_bytes: list[int], write_mock: WriteFunctionMock): @@ -346,7 +346,7 @@ def test_write_utf_limit(self, write_mock: WriteFunctionMock): with pytest.raises(ValueError, match="Maximum character limit for writing strings is 32767 characters."): self.writer.write_utf("a" * (32768)) - def test_write_optional_true(self, method_mock: Union[Mock, AsyncMock], write_mock: WriteFunctionMock): + def test_write_optional_true(self, method_mock: Mock | AsyncMock, write_mock: WriteFunctionMock): """Writing non-None value should write True and run the writer function.""" mock_v = Mock() mock_f = method_mock() @@ -354,7 +354,7 @@ def test_write_optional_true(self, method_mock: Union[Mock, AsyncMock], write_mo mock_f.assert_called_once_with(mock_v) write_mock.assert_has_data(bytearray([1])) - def test_write_optional_false(self, method_mock: Union[Mock, AsyncMock], write_mock: WriteFunctionMock): + def test_write_optional_false(self, method_mock: Mock | AsyncMock, write_mock: WriteFunctionMock): """Writing None value should write False and skip running the writer function.""" mock_f = method_mock() self.writer.write_optional(None, mock_f) @@ -365,7 +365,7 @@ def test_write_optional_false(self, method_mock: Union[Mock, AsyncMock], write_m class ReaderTests(ABC): """This class holds tests for both sync and async versions of reader.""" - reader: Union[BaseSyncReader, BaseAsyncReader] + reader: BaseSyncReader | BaseAsyncReader @classmethod @abstractmethod @@ -374,7 +374,7 @@ def setup_class(cls): ... @pytest.fixture() - def method_mock(self) -> Union[Mock, AsyncMock]: + def method_mock(self) -> Mock | AsyncMock: """Returns the appropriate type of mock, supporting both sync and async modes.""" if isinstance(self.reader, BaseSyncReader): return Mock @@ -394,7 +394,7 @@ def autopatch(self, monkeypatch: pytest.MonkeyPatch): patch_path = "mcproto.protocol.base_io.BaseAsyncReader" mock_type = AsyncMock - def autopatch(function_name: str) -> Union[Mock, AsyncMock]: + def autopatch(function_name: str) -> Mock | AsyncMock: mock_f = mock_type() monkeypatch.setattr(f"{patch_path}.{function_name}", mock_f) return mock_f @@ -523,7 +523,7 @@ def test_read_ascii(self, read_bytes: list[int], expected_string: str, read_mock ([len("test"), *list(map(ord, "test"))], "test"), ([len("a" * 100), *list(map(ord, "a" * 100))], "a" * 100), ([0], ""), - ([18] + [int(x) for x in "नमस्ते".encode("utf-8")], "नमस्ते"), + ([18] + [int(x) for x in "नमस्ते".encode()], "नमस्ते"), ], ) def test_read_utf(self, read_bytes: list[int], expected_string: str, read_mock: ReadFunctionMock): @@ -548,14 +548,14 @@ def test_read_utf_limit(self, read_bytes: list[int], read_mock: ReadFunctionMock with pytest.raises(IOError): self.reader.read_utf() - def test_read_optional_true(self, method_mock: Union[Mock, AsyncMock], read_mock: ReadFunctionMock): + def test_read_optional_true(self, method_mock: Mock | AsyncMock, read_mock: ReadFunctionMock): """Reading optional should run reader function when first bool is True.""" mock_f = method_mock() read_mock.combined_data = bytearray([1]) self.reader.read_optional(mock_f) mock_f.assert_called_once_with() - def test_read_optional_false(self, method_mock: Union[Mock, AsyncMock], read_mock: ReadFunctionMock): + def test_read_optional_false(self, method_mock: Mock | AsyncMock, read_mock: ReadFunctionMock): """Reading optional should not run reader function when first bool is False.""" mock_f = method_mock() read_mock.combined_data = bytearray([0]) diff --git a/tests/mcproto/test_connection.py b/tests/mcproto/test_connection.py index 2409cbeb..8056865d 100644 --- a/tests/mcproto/test_connection.py +++ b/tests/mcproto/test_connection.py @@ -3,7 +3,6 @@ import asyncio import errno import socket -from typing import Optional from unittest.mock import MagicMock import pytest @@ -16,7 +15,7 @@ class MockSocket(CustomMockMixin, MagicMock): spec_set = socket.socket - def __init__(self, *args, read_data: Optional[bytearray] = None, **kwargs) -> None: + def __init__(self, *args, read_data: bytearray | None = None, **kwargs) -> None: super().__init__(*args, **kwargs) self.mock_add_spec(["_recv", "_send", "_closed"]) self._recv = ReadFunctionMock(combined_data=read_data) @@ -61,7 +60,7 @@ def close(self) -> None: class MockStreamReader(CustomMockMixin, MagicMock): spec_set = asyncio.StreamReader - def __init__(self, *args, read_data: Optional[bytearray] = None, **kwargs) -> None: + def __init__(self, *args, read_data: bytearray | None = None, **kwargs) -> None: super().__init__(*args, **kwargs) self.mock_add_spec(["_read"]) self._read = ReadFunctionAsyncMock(combined_data=read_data) @@ -71,7 +70,7 @@ def read(self, length: int) -> bytearray: class TestTCPSyncConnection: - def make_connection(self, read_data: Optional[bytearray] = None) -> TCPSyncConnection[MockSocket]: + def make_connection(self, read_data: bytearray | None = None) -> TCPSyncConnection[MockSocket]: if read_data is not None: read_data = read_data.copy() @@ -125,7 +124,7 @@ def test_socket_close_contextmanager(self): class TestTCPAsyncConnection: def make_connection( self, - read_data: Optional[bytearray] = None, + read_data: bytearray | None = None, ) -> TCPAsyncConnection[MockStreamReader, MockStreamWriter]: if read_data is not None: read_data = read_data.copy() From 24503f5acf2593446adbb3633da6ca1dd79bd77a Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 16 Jul 2023 01:37:09 +0200 Subject: [PATCH 07/11] Enable PL linter rules This enables all ruff's pylint (PL) rules. However PLR2004 (using unnamed numerical constants) is ignored, as these kinds of constants are used all over the codebase, in comparisons, etc. As mcproto is a fairly low level library, this is not surprising, and in most cases, it's easy to understand what the numerical constant is in the context of that comparison. --- pyproject.toml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 63236f76..c67e925c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,18 +119,20 @@ select = [ "SIM", # flake8-simplify "TID", # flake8-tidy-imports "PTH", # flake8-use-pathlib + "PL", # pylint "RUF", # ruff-specific rules "UP", # pyupgrade ] ignore = [ - "ANN002", # Missing type annotation for *args - "ANN003", # Missing type annotation for **kwargs - "ANN101", # Missing type annotation for self in method - "ANN102", # Missing type annotation for cls in classmethod - "ANN204", # Missing return type annotation for special method - "PT011", # pytest.raises without match parameter is too broad - "UP024", # Using errors that alias OSError + "ANN002", # Missing type annotation for *args + "ANN003", # Missing type annotation for **kwargs + "ANN101", # Missing type annotation for self in method + "ANN102", # Missing type annotation for cls in classmethod + "ANN204", # Missing return type annotation for special method + "PT011", # pytest.raises without match parameter is too broad + "UP024", # Using errors that alias OSError + "PLR2004", # Using unnamed numerical constants ] [tool.ruff.extend-per-file-ignores] From 49261fcc9aeafd546b41df4221e9d252421481b9 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 16 Jul 2023 01:43:33 +0200 Subject: [PATCH 08/11] Enable PGH linter rules This enables all ruff's pygrep-hooks (PGH) rules. However PGH003 (using specific rule codes in type ignores) is ignored, as we're using pyright rather than mypy, and while there are some rulecodes for pyright and we could be more specific, most of the violations just fall under the same (general typing issues) category, which isn't very helpful for categorizing the issue. Because of that, this would just be annoying to do without any huge benefit for it. --- .pre-commit-config.yaml | 6 ------ pyproject.toml | 2 ++ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4c627fed..f221e2c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,12 +11,6 @@ repos: - id: mixed-line-ending args: [--fix=lf] - - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.9.0 - hooks: - - id: python-check-blanket-noqa # Enforce noqa annotations (noqa: F401,W203) - - id: python-use-type-annotations # Enforce type annotations instead of type comments - - repo: local hooks: - id: black diff --git a/pyproject.toml b/pyproject.toml index c67e925c..67596997 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -119,6 +119,7 @@ select = [ "SIM", # flake8-simplify "TID", # flake8-tidy-imports "PTH", # flake8-use-pathlib + "PGH", # pygrep-hooks "PL", # pylint "RUF", # ruff-specific rules "UP", # pyupgrade @@ -133,6 +134,7 @@ ignore = [ "PT011", # pytest.raises without match parameter is too broad "UP024", # Using errors that alias OSError "PLR2004", # Using unnamed numerical constants + "PGH003", # Using specific rule codes in type ignores ] [tool.ruff.extend-per-file-ignores] From 89399c17bf2be0fb3e362143b8034f9137f33e17 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 16 Jul 2023 01:50:15 +0200 Subject: [PATCH 09/11] Add todo to unignore PT011 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 67596997..24fa083c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,7 +131,7 @@ ignore = [ "ANN101", # Missing type annotation for self in method "ANN102", # Missing type annotation for cls in classmethod "ANN204", # Missing return type annotation for special method - "PT011", # pytest.raises without match parameter is too broad + "PT011", # pytest.raises without match parameter is too broad # TODO: Unignore this "UP024", # Using errors that alias OSError "PLR2004", # Using unnamed numerical constants "PGH003", # Using specific rule codes in type ignores From 60b019d62a9338bf5eb349e7f8074a347ced74b0 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 16 Jul 2023 02:05:31 +0200 Subject: [PATCH 10/11] Enable INT linter rules This enables all ruff's flake8-gettext (INT) rules. --- pyproject.toml | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 24fa083c..89962557 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,30 +99,31 @@ target-version = "py38" line-length = 119 select = [ - "F", # Pyflakes - "W", # Pycodestyle (warnigns) - "E", # Pycodestyle (errors) - "N", # pep8-naming - "YTT", # flake8-2020 - "ANN", # flake8-annotations - "ASYNC", # flake8-async - "S", # flake8-bandit - "B", # flake8-bugbear - "A", # flake8-builtins - "C4", # flake8-comprehensions - "FA", # flake8-future-annotations - "T20", # flake8-print - "PT", # flake8-pytest-style - "Q", # flake8-quotes - "RSE", # flake8-raise - "RET", # flake8-return - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "PTH", # flake8-use-pathlib - "PGH", # pygrep-hooks - "PL", # pylint - "RUF", # ruff-specific rules - "UP", # pyupgrade + "F", # Pyflakes + "W", # Pycodestyle (warnigns) + "E", # Pycodestyle (errors) + "N", # pep8-naming + "YTT", # flake8-2020 + "ANN", # flake8-annotations + "ASYNC", # flake8-async + "S", # flake8-bandit + "B", # flake8-bugbear + "A", # flake8-builtins + "C4", # flake8-comprehensions + "FA", # flake8-future-annotations + "T20", # flake8-print + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "INT", # flake8-gettext + "PTH", # flake8-use-pathlib + "PGH", # pygrep-hooks + "PL", # pylint + "RUF", # ruff-specific rules + "UP", # pyupgrade ] ignore = [ From 0b49d66d2df1094056ae7c593c0096225f9c1180 Mon Sep 17 00:00:00 2001 From: ItsDrike Date: Sun, 16 Jul 2023 02:06:17 +0200 Subject: [PATCH 11/11] Add changelog fragment for 154 --- changes/154.internal.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 changes/154.internal.md diff --git a/changes/154.internal.md b/changes/154.internal.md new file mode 100644 index 00000000..90b049ed --- /dev/null +++ b/changes/154.internal.md @@ -0,0 +1,11 @@ +Enforce various new ruff linter rules: + +- **PGH:** pygrep-hooks (replaces pre-commit version) +- **PL:** pylint (bunch of typing related linter rules) +- **UP:** pyupgrade (forces use of the newest possible standards, depending on target version) +- **RET:** flake8-return (various linter rules related to function returns) +- **Q:** flake8-quotes (always use double quotes) +- **ASYNC:** flake8-async (report blocking operations in async functions) +- **INT:** flake-gettext (gettext related linting rules) +- **PTH:** flake8-use-pathlib (always prefer pathlib alternatives to the os ones) +- **RUF:** ruff custom rules (various additional rules created by the ruff linter team)