From 266ab43ce9668defa0ef72c6bb8ce529b9fde173 Mon Sep 17 00:00:00 2001 From: linuxdaemon Date: Sun, 26 Oct 2025 15:57:46 +0000 Subject: [PATCH] refactor: drop python 3.9 support BREAKING CHANGE: --- .github/workflows/python-tests.yml | 1 - .pre-commit-config.yaml | 8 +++- asyncirc/protocol.py | 71 +++++++++++------------------- asyncirc/server.py | 45 ++++++++++--------- asyncirc/util/backoff.py | 2 +- pyproject.toml | 22 ++++----- tests/test_protocol.py | 43 +++++++++--------- 7 files changed, 89 insertions(+), 103 deletions(-) diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index a59ba7b..755d08d 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -13,7 +13,6 @@ jobs: fail-fast: false matrix: python-version: - - '3.9' - '3.10' - '3.11' - '3.12' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f21145f..71c6579 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,7 +55,7 @@ repos: rev: 97be693bf18bc2f050667dd282d243e2824b81e2 # frozen: 1.0.6 - hooks: - args: - - --py39-plus + - --py310-plus id: pyupgrade repo: https://github.com/asottile/pyupgrade rev: f90119b1b8bd9e46949d9592972b2c4c27d62a97 # frozen: v3.21.0 @@ -85,9 +85,13 @@ repos: - python repo: local - repo: https://github.com/andreoliwa/nitpick - rev: "a5532d554f2c9035bb05ec14ee1bf31469e0a563" # frozen: v0.37.0 + rev: "a1373f03f5a9394c75cc03e5da1b0cb6ff615dbb" # frozen: v0.38.0 hooks: - id: nitpick +- repo: https://github.com/PyCQA/autoflake + rev: '0544741e2b4a22b472d9d93e37d4ea9153820bb1' # frozen: v2.3.1 + hooks: + - id: autoflake minimum_pre_commit_version: 2.18.0 default_install_hook_types: - pre-commit diff --git a/asyncirc/protocol.py b/asyncirc/protocol.py index 582e4a0..42c5399 100644 --- a/asyncirc/protocol.py +++ b/asyncirc/protocol.py @@ -7,20 +7,10 @@ import time from asyncio import Protocol, Task from collections import defaultdict -from collections.abc import Coroutine, Sequence +from collections.abc import Callable, Coroutine, Sequence from enum import IntEnum, auto, unique from itertools import cycle -from typing import ( - TYPE_CHECKING, - Callable, - Dict, - Final, - List, - Optional, - Tuple, - Union, - cast, -) +from typing import TYPE_CHECKING, Final, Optional, cast from irclib.parser import Cap, CapList, Message @@ -53,13 +43,13 @@ async def _internal_ping(conn: "IrcProtocol", message: "Message") -> None: conn.send(f"PONG {message.parameters}") -def _handle_cap_list(conn: "IrcProtocol", caplist: "List[Cap]") -> None: +def _handle_cap_list(conn: "IrcProtocol", caplist: "list[Cap]") -> None: if conn.logger: conn.logger.info("Current Capabilities: %s", caplist) def _handle_cap_del( - conn: "IrcProtocol", caplist: "List[Cap]", server: "ConnectedServer" + conn: "IrcProtocol", caplist: "list[Cap]", server: "ConnectedServer" ) -> None: if conn.logger: conn.logger.info("Capabilities removed: %s", caplist) @@ -72,7 +62,7 @@ def _handle_cap_del( def _handle_cap_new( conn: "IrcProtocol", message: "Message", - caplist: "List[Cap]", + caplist: "list[Cap]", server: "ConnectedServer", ) -> None: if conn.logger: @@ -90,7 +80,7 @@ def _handle_cap_new( async def _handle_cap_reply( conn: "IrcProtocol", message: "Message", - caplist: "List[Cap]", + caplist: "list[Cap]", server: "ConnectedServer", ) -> None: enabled = message.parameters[1] == "ACK" @@ -109,7 +99,7 @@ async def _handle_cap_reply( def _handle_cap_ls( conn: "IrcProtocol", message: "Message", - caplist: "List[Cap]", + caplist: "list[Cap]", server: "ConnectedServer", ) -> None: for cap in caplist: @@ -170,7 +160,7 @@ async def _do_sasl(conn: "IrcProtocol", cap: Cap) -> None: return if cap.value is not None: - supported_mechs: Optional[list[str]] = cap.value.split(",") + supported_mechs: list[str] | None = cap.value.split(",") else: supported_mechs = None @@ -244,11 +234,11 @@ def __init__( self, servers: Sequence["BaseServer"], nick: str, - user: Optional[str] = None, - realname: Optional[str] = None, - certpath: Optional[str] = None, - sasl_auth: Optional[tuple[str, str]] = None, - sasl_mech: Optional[SASLMechanism] = None, + user: str | None = None, + realname: str | None = None, + certpath: str | None = None, + sasl_auth: tuple[str, str] | None = None, + sasl_mech: SASLMechanism | None = None, logger: Optional["Logger"] = None, loop: Optional["AbstractEventLoop"] = None, *, @@ -290,19 +280,14 @@ def __init__( int, tuple[ str, - Callable[ - ["IrcProtocol", "Message"], Coroutine[None, None, None] - ], + Callable[[IrcProtocol, Message], Coroutine[None, None, None]], ], ] = {} self.cap_handlers: dict[ str, list[ - Optional[ - Callable[ - ["IrcProtocol", "Cap"], Coroutine[None, None, None] - ] - ] + None + | (Callable[[IrcProtocol, Cap], Coroutine[None, None, None]]) ], ] = defaultdict(list) @@ -316,9 +301,7 @@ def __init__( self.register("005", _isupport_handler) self.register_cap("sasl", _do_sasl) - self._pinger: Optional[Task[None]] = self.loop.create_task( - self.pinger() - ) + self._pinger: Task[None] | None = self.loop.create_task(self.pinger()) def __del__(self) -> None: """Automatically close connection on garbage collection.""" @@ -417,9 +400,9 @@ def unregister(self, hook_id: int) -> None: def register_cap( self, cap: str, - handler: Optional[ + handler: None | ( Callable[["IrcProtocol", "Cap"], Coroutine[None, None, None]] - ] = None, + ) = None, ) -> None: """Register a CAP handler. @@ -430,8 +413,8 @@ def register_cap( self.cap_handlers[cap].append(handler) async def wait_for( - self, *cmds: str, timeout: Optional[int] = None - ) -> Optional[Message]: + self, *cmds: str, timeout: int | None = None + ) -> Message | None: """Wait for a matching command from the server. Wait for a specific command from the server, optionally returning after [timeout] seconds. @@ -439,7 +422,7 @@ async def wait_for( if not cmds: return None - fut: "asyncio.Future[Message]" = self.loop.create_future() + fut: asyncio.Future[Message] = self.loop.create_future() async def _wait(_conn: "IrcProtocol", message: "Message") -> None: if not fut.done(): @@ -457,14 +440,12 @@ async def _wait(_conn: "IrcProtocol", message: "Message") -> None: return result - async def _send(self, text: Union[str, bytes]) -> None: + async def _send(self, text: str | bytes) -> None: if not self.connected: await self._connected_future if isinstance(text, str): text = text.encode() - elif isinstance(text, memoryview): - text = text.tobytes() if self.logger: self.logger.info(">> %s", text.decode()) @@ -475,7 +456,7 @@ async def _send(self, text: Union[str, bytes]) -> None: self._transport.write(text + b"\r\n") - def send(self, text: Union[str, bytes]) -> None: + def send(self, text: str | bytes) -> None: """Send a raw line to the server.""" asyncio.run_coroutine_threadsafe(self._send(text), self.loop) @@ -483,7 +464,7 @@ def send_command(self, msg: Message) -> None: """Send an irclib Message object to the server.""" return self.send(str(msg)) - def quit(self, reason: Optional[str] = None) -> None: + def quit(self, reason: str | None = None) -> None: """Quit the IRC connection with an optional reason.""" if not self._quitting: self._quitting = True @@ -509,7 +490,7 @@ def connection_made(self, transport: "asyncio.BaseTransport") -> None: self.send(f"NICK {self.nick}") self.send(f"USER {self.user} 0 * :{self.realname}") - def connection_lost(self, exc: Optional[Exception]) -> None: + def connection_lost(self, exc: Exception | None) -> None: """Connection to the IRC server has been lost.""" self._transport = None self._connected = False diff --git a/asyncirc/server.py b/asyncirc/server.py index ee889a6..fb13da3 100644 --- a/asyncirc/server.py +++ b/asyncirc/server.py @@ -3,7 +3,8 @@ import asyncio import ssl import warnings -from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, TypeVar +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, TypeVar if TYPE_CHECKING: from irclib.parser import Cap @@ -25,10 +26,10 @@ class BaseServer: def __init__( self, *, - password: Optional[str] = None, + password: str | None = None, is_ssl: bool = False, - ssl_ctx: Optional[ssl.SSLContext] = None, - certpath: Optional[str] = None, + ssl_ctx: ssl.SSLContext | None = None, + certpath: str | None = None, ) -> None: """Set server connection options. @@ -45,7 +46,7 @@ def __init__( if certpath: ssl_ctx.load_cert_chain(certpath) - self.ssl_ctx: Optional[ssl.SSLContext] = ssl_ctx + self.ssl_ctx: ssl.SSLContext | None = ssl_ctx else: self.ssl_ctx = None @@ -60,8 +61,8 @@ async def connect( self, protocol_factory: Callable[[], _ProtoT], *, - loop: Optional[asyncio.AbstractEventLoop] = None, - ssl: Optional[ssl.SSLContext] = None, + loop: asyncio.AbstractEventLoop | None = None, + ssl: ssl.SSLContext | None = None, ) -> tuple[asyncio.Transport, _ProtoT]: """Internal connect implementation. @@ -82,7 +83,7 @@ async def do_connect( self, protocol_factory: Callable[[], _ProtoT], *, - loop: Optional[asyncio.AbstractEventLoop] = None, + loop: asyncio.AbstractEventLoop | None = None, ) -> tuple[asyncio.Transport, _ProtoT]: """Wrapper for internal connect implementation. @@ -109,9 +110,9 @@ def __init__( host: str, port: int, is_ssl: bool = False, - password: Optional[str] = None, - ssl_ctx: Optional[ssl.SSLContext] = None, - certpath: Optional[str] = None, + password: str | None = None, + ssl_ctx: ssl.SSLContext | None = None, + certpath: str | None = None, ) -> None: """Create TCP server configuration. @@ -134,8 +135,8 @@ async def connect( self, protocol_factory: Callable[[], _ProtoT], *, - loop: Optional[asyncio.AbstractEventLoop] = None, - ssl: Optional[ssl.SSLContext] = None, + loop: asyncio.AbstractEventLoop | None = None, + ssl: ssl.SSLContext | None = None, ) -> tuple[asyncio.Transport, _ProtoT]: """TCP server connection implementation. @@ -170,9 +171,9 @@ def __init__( *, path: str, is_ssl: bool = False, - password: Optional[str] = None, - ssl_ctx: Optional[ssl.SSLContext] = None, - certpath: Optional[str] = None, + password: str | None = None, + ssl_ctx: ssl.SSLContext | None = None, + certpath: str | None = None, ) -> None: """Configure UNIX socket based server connection. @@ -193,8 +194,8 @@ async def connect( self, protocol_factory: Callable[[], _ProtoT], *, - loop: Optional[asyncio.AbstractEventLoop] = None, - ssl: Optional[ssl.SSLContext] = None, + loop: asyncio.AbstractEventLoop | None = None, + ssl: ssl.SSLContext | None = None, ) -> tuple[asyncio.Transport, _ProtoT]: """Connect to UNIX socket. @@ -229,7 +230,7 @@ def __init__( host: str, port: int, is_ssl: bool = False, - password: Optional[str] = None, + password: str | None = None, ) -> None: """Create basic server configuration. @@ -268,7 +269,7 @@ def __init__(self, server: "BaseServer") -> None: self.connection = server self.is_ssl = server.is_ssl self.password = server.password - self.isupport_tokens: dict[str, Optional[str]] = {} - self.caps: dict[str, tuple[Cap, Optional[bool]]] = {} - self.server_name: Optional[str] = None + self.isupport_tokens: dict[str, str | None] = {} + self.caps: dict[str, tuple[Cap, bool | None]] = {} + self.server_name: str | None = None self.data: dict[str, Any] = {} diff --git a/asyncirc/util/backoff.py b/asyncirc/util/backoff.py index 547b867..1de7d5f 100644 --- a/asyncirc/util/backoff.py +++ b/asyncirc/util/backoff.py @@ -2,7 +2,7 @@ import asyncio import random -from typing import Callable +from collections.abc import Callable __all__ = ("AsyncDelayer",) diff --git a/pyproject.toml b/pyproject.toml index 4dc87b5..37e7143 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,19 +8,18 @@ dynamic = ["version"] description = "A simple asyncio.Protocol implementation designed for IRC" readme = "README.md" license = "MIT" -requires-python = ">=3.9" +requires-python = ">=3.10" authors = [{ name = "linuxdaemon", email = "linuxdaemon.irc@gmail.com" }] keywords = ["async-irc", "asyncio", "asyncirc", "irc", "irc-framework"] classifiers = [ "Development Status :: 3 - Alpha", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] -dependencies = ["py-irclib>=0.4.0", "typing_extensions"] +dependencies = ["py-irclib>=0.8.0", "typing_extensions"] [project.urls] Homepage = "https://github.com/TotallyNotRobots/async-irc" @@ -70,24 +69,22 @@ type = "container" dependencies = ["coverage[toml]>=6.5", "pytest>=6.0", "pytest-asyncio"] [[tool.hatch.envs.testall.matrix]] -python = ["3.9", "3.10", "3.11", "3.12"] +python = ["3.10", "3.11", "3.12"] [tool.isort] profile = "black" line_length = 80 -include_trailing_comma = true -use_parentheses = true known_first_party = ["asyncirc", "tests"] float_to_top = true [tool.black] line-length = 80 -target-version = ["py39"] +target-version = ["py310"] include = '\.pyi?$' [tool.ruff] line-length = 80 -target-version = 'py39' +target-version = 'py310' [tool.ruff.format] docstring-code-format = true @@ -148,7 +145,7 @@ line-length = 120 [tool.mypy] namespace_packages = true -python_version = "3.9" +python_version = "3.10" warn_unused_configs = true strict = true strict_optional = true @@ -207,5 +204,8 @@ major_version_zero = true annotated_tag = true [tool.nitpick] -style = [ - "gh://TotallyNotRobots/nitpick/lib-style-3.9.toml",] +style = ["gh://TotallyNotRobots/nitpick/lib-style-3.10.toml"] + +[tool.autoflake] +remove-all-unused-imports = true +in-place = true diff --git a/tests/test_protocol.py b/tests/test_protocol.py index 607601b..f394aa9 100644 --- a/tests/test_protocol.py +++ b/tests/test_protocol.py @@ -3,9 +3,9 @@ import asyncio import logging import ssl -from collections.abc import Mapping +from collections.abc import Callable, Mapping from ssl import SSLContext -from typing import Any, Callable, List, Optional, Tuple, TypeVar, Union +from typing import Any, TypeVar from irclib.parser import Message @@ -20,8 +20,8 @@ def __init__( self, server: "MockServer", protocol: asyncio.BaseProtocol, - extra: Optional[Mapping[str, Any]] = None, - sasl_cap_mechs: Optional[list[str]] = None, + extra: Mapping[str, Any] | None = None, + sasl_cap_mechs: list[str] | None = None, ) -> None: """Create mock transport for testing. @@ -35,7 +35,8 @@ def __init__( super().__init__(extra) self._server = server if not isinstance(protocol, IrcProtocol): # pragma: no cover - raise TypeError(f"Protocol is wrong type: {type(protocol)}") + msg = f"Protocol is wrong type: {type(protocol)}" + raise TypeError(msg) self._protocol = protocol self._write_buffer = b"" @@ -48,7 +49,7 @@ def __init__( self._registered = False self._sasl_cap_mechs = sasl_cap_mechs - def write(self, data: Union[bytes, bytearray, memoryview]) -> None: + def write(self, data: bytes | bytearray | memoryview) -> None: """'Write data to the server. This just handles the incoming data and sends responses directly to the Protocol. @@ -128,11 +129,11 @@ class MockServer(BaseServer): def __init__( self, *, - password: Optional[str] = None, + password: str | None = None, is_ssl: bool = False, - ssl_ctx: Optional[SSLContext] = None, - certpath: Optional[str] = None, - sasl_cap_mechs: Optional[list[str]] = None, + ssl_ctx: SSLContext | None = None, + certpath: str | None = None, + sasl_cap_mechs: list[str] | None = None, ) -> None: """Configure mock server. @@ -147,7 +148,7 @@ def __init__( ) self.lines: list[tuple[str, str]] = [] self.in_buffer = b"" - self.transport: Optional[MockTransport] = None + self.transport: MockTransport | None = None self.sasl_cap_mechs = sasl_cap_mechs def send_line(self, msg: Message) -> None: @@ -164,8 +165,8 @@ async def connect( self, protocol_factory: Callable[[], _ProtoT], *, - loop: Optional[asyncio.AbstractEventLoop] = None, - ssl: Optional[ssl.SSLContext] = None, + loop: asyncio.AbstractEventLoop | None = None, + ssl: ssl.SSLContext | None = None, ) -> tuple[asyncio.Transport, _ProtoT]: """Mock connection implementation. @@ -187,9 +188,9 @@ async def connect( async def test_sasl() -> None: """Test sasl flow.""" - fut: "asyncio.Future[None]" = asyncio.Future() + fut = asyncio.Future[None]() - async def _on_001(_conn: "IrcProtocol", _msg: "Message") -> None: + async def _on_001(_conn: IrcProtocol, _msg: Message) -> None: _conn.send_command(Message.parse("PRIVMSG #foo :bar")) fut.set_result(None) @@ -292,9 +293,9 @@ async def _on_001(_conn: "IrcProtocol", _msg: "Message") -> None: async def test_sasl_multiple_mechs() -> None: """Test sasl flow.""" - fut: "asyncio.Future[None]" = asyncio.Future() + fut = asyncio.Future[None]() - async def _on_001(_conn: "IrcProtocol", _msg: "Message") -> None: + async def _on_001(_conn: IrcProtocol, _msg: Message) -> None: _conn.send_command(Message.parse("PRIVMSG #foo :bar")) fut.set_result(None) @@ -397,9 +398,9 @@ async def _on_001(_conn: "IrcProtocol", _msg: "Message") -> None: async def test_sasl_unsupported_mechs() -> None: """Test sasl flow.""" - fut: "asyncio.Future[None]" = asyncio.Future() + fut = asyncio.Future[None]() - async def _on_001(_conn: "IrcProtocol", _msg: "Message") -> None: + async def _on_001(_conn: IrcProtocol, _msg: Message) -> None: _conn.send_command(Message.parse("PRIVMSG #foo :bar")) fut.set_result(None) @@ -486,9 +487,9 @@ async def _on_001(_conn: "IrcProtocol", _msg: "Message") -> None: async def test_connect_ssl() -> None: """Test sasl flow.""" - fut: "asyncio.Future[None]" = asyncio.Future() + fut = asyncio.Future[None]() - async def _on_001(_conn: "IrcProtocol", _msg: "Message") -> None: + async def _on_001(_conn: IrcProtocol, _msg: Message) -> None: _conn.send_command(Message.parse("PRIVMSG #foo :bar")) fut.set_result(None)