EOL of Py3.9 (#1726)
Some checks failed
Tests / tests (macos-latest, 3.10) (push) Has been cancelled
Tests / tests (macos-latest, 3.11) (push) Has been cancelled
Tests / tests (macos-latest, 3.12) (push) Has been cancelled
Tests / tests (macos-latest, 3.13) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.10) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.11) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.12) (push) Has been cancelled
Tests / tests (ubuntu-latest, 3.13) (push) Has been cancelled
Tests / tests (windows-latest, 3.10) (push) Has been cancelled
Tests / tests (windows-latest, 3.11) (push) Has been cancelled
Tests / tests (windows-latest, 3.12) (push) Has been cancelled
Tests / tests (windows-latest, 3.13) (push) Has been cancelled
Tests / pypy-tests (macos-latest, pypy3.10) (push) Has been cancelled
Tests / pypy-tests (macos-latest, pypy3.11) (push) Has been cancelled
Tests / pypy-tests (ubuntu-latest, pypy3.10) (push) Has been cancelled
Tests / pypy-tests (ubuntu-latest, pypy3.11) (push) Has been cancelled

* Drop py3.9 and pypy3.9

Add pypy3.11 (testing) into `tests.yml`

Remove py3.9 from matrix in `tests.yml`

Refactor not auto-gen code to be compatible with py3.10+, droping ugly 3.9 annotation.

Replace some `from typing` imports to `from collections.abc`, due to deprecation

Add `from __future__ import annotations` and `if TYPE_CHECKING:` where possible

Add some `noqa` to calm down Ruff in some places, if Ruff will be used as default linting+formatting tool in future

Replace some relative imports to absolute

Sort `__all__` tuples in `__init__.py` and some other `.py` files

Sort `__slots__` tuples in classes

Split raises into `msg` and `raise` (`EM101`, `EM102`) to not duplicate error message in the traceback

Add `Self` from `typing_extenstion` where possible

Resolve typing problem in `aiogram/filters/command.py:18`

Concatenate nested `if` statements

Convert `HandlerContainer` into a dataclass in `aiogram/fsm/scene.py`

Bump tests docker-compose.yml `redis:6-alpine` -> `redis:8-alpine`

Bump tests docker-compose.yml `mongo:7.0.6` -> `mongo:8.0.14`

Bump pre-commit-config `black==24.4.2` -> `black==25.9.0`

Bump pre-commit-config `ruff==0.5.1` -> `ruff==0.13.3`

Update Makefile lint for ruff to show fixes

Add `make outdated` into Makefile

Use `pathlib` instead of `os.path`

Bump `redis[hiredis]>=5.0.1,<5.3.0` -> `redis[hiredis]>=6.2.0,<7`

Bump `cryptography>=43.0.0` -> `cryptography>=46.0.0` due to security reasons

Bump `pytz~=2023.3` -> `pytz~=2025.2`

Bump `pycryptodomex~=3.19.0` -> `pycryptodomex~=3.23.0` due to security reasons

Bump linting and formatting tools

* Add `1726.removal.rst`

* Update aiogram/utils/dataclass.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update aiogram/filters/callback_data.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update 1726.removal.rst

* Remove `outdated` from Makefile

* Add `__slots__` to `HandlerContainer`

* Remove unused imports

* Add `@dataclass` with `slots=True` to `HandlerContainer`

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Andrew 2025-10-06 19:19:23 +03:00 committed by GitHub
parent ab32296d07
commit df7b16d5b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
94 changed files with 1383 additions and 1215 deletions

View file

@ -1,4 +1,4 @@
from typing import Any, Dict, Optional, Union
from typing import Any
from aiogram import Router
from aiogram.filters import Filter
@ -8,7 +8,7 @@ router = Router(name=__name__)
class HelloFilter(Filter):
def __init__(self, name: Optional[str] = None) -> None:
def __init__(self, name: str | None = None) -> None:
self.name = name
async def __call__(
@ -16,7 +16,7 @@ class HelloFilter(Filter):
message: Message,
event_from_user: User,
# Filters also can accept keyword parameters like in handlers
) -> Union[bool, Dict[str, Any]]:
) -> bool | dict[str, Any]:
if message.text.casefold() == "hello":
# Returning a dictionary that will update the context data
return {"name": event_from_user.mention_html(name=self.name)}
@ -25,6 +25,7 @@ class HelloFilter(Filter):
@router.message(HelloFilter())
async def my_handler(
message: Message, name: str # Now we can accept "name" as named parameter
message: Message,
name: str, # Now we can accept "name" as named parameter
) -> Any:
return message.answer("Hello, {name}!".format(name=name))
return message.answer(f"Hello, {name}!")

View file

@ -75,11 +75,13 @@ async def handle_set_age(message: types.Message, command: CommandObject) -> None
# To get the command arguments you can use `command.args` property.
age = command.args
if not age:
raise InvalidAge("No age provided. Please provide your age as a command argument.")
msg = "No age provided. Please provide your age as a command argument."
raise InvalidAge(msg)
# If the age is invalid, raise an exception.
if not age.isdigit():
raise InvalidAge("Age should be a number")
msg = "Age should be a number"
raise InvalidAge(msg)
# If the age is valid, send a message to the user.
age = int(age)
@ -95,7 +97,8 @@ async def handle_set_name(message: types.Message, command: CommandObject) -> Non
# To get the command arguments you can use `command.args` property.
name = command.args
if not name:
raise InvalidName("Invalid name. Please provide your name as a command argument.")
msg = "Invalid name. Please provide your name as a command argument."
raise InvalidName(msg)
# If the name is valid, send a message to the user.
await message.reply(text=f"Your name is {name}")

View file

@ -2,7 +2,7 @@ import asyncio
import logging
import sys
from os import getenv
from typing import Any, Dict
from typing import Any
from aiogram import Bot, Dispatcher, F, Router, html
from aiogram.client.default import DefaultBotProperties
@ -66,7 +66,7 @@ async def process_name(message: Message, state: FSMContext) -> None:
[
KeyboardButton(text="Yes"),
KeyboardButton(text="No"),
]
],
],
resize_keyboard=True,
),
@ -106,13 +106,13 @@ async def process_language(message: Message, state: FSMContext) -> None:
if message.text.casefold() == "python":
await message.reply(
"Python, you say? That's the language that makes my circuits light up! 😉"
"Python, you say? That's the language that makes my circuits light up! 😉",
)
await show_summary(message=message, data=data)
async def show_summary(message: Message, data: Dict[str, Any], positive: bool = True) -> None:
async def show_summary(message: Message, data: dict[str, Any], positive: bool = True) -> None:
name = data["name"]
language = data.get("language", "<something unexpected>")
text = f"I'll keep in mind that, {html.quote(name)}, "
@ -124,7 +124,7 @@ async def show_summary(message: Message, data: Dict[str, Any], positive: bool =
await message.answer(text=text, reply_markup=ReplyKeyboardRemove())
async def main():
async def main() -> None:
# Initialize Bot instance with default bot properties which will be passed to all API calls
bot = Bot(token=TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))

View file

@ -1,7 +1,7 @@
import logging
import sys
from os import getenv
from typing import Any, Dict, Union
from typing import Any
from aiohttp import web
from finite_state_machine import form_router
@ -34,7 +34,7 @@ REDIS_DSN = "redis://127.0.0.1:6479"
OTHER_BOTS_URL = f"{BASE_URL}{OTHER_BOTS_PATH}"
def is_bot_token(value: str) -> Union[bool, Dict[str, Any]]:
def is_bot_token(value: str) -> bool | dict[str, Any]:
try:
validate_token(value)
except TokenValidationError:
@ -54,11 +54,11 @@ async def command_add_bot(message: Message, command: CommandObject, bot: Bot) ->
return await message.answer(f"Bot @{bot_user.username} successful added")
async def on_startup(dispatcher: Dispatcher, bot: Bot):
async def on_startup(dispatcher: Dispatcher, bot: Bot) -> None:
await bot.set_webhook(f"{BASE_URL}{MAIN_BOT_PATH}")
def main():
def main() -> None:
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
session = AiohttpSession()
bot_settings = {"session": session, "parse_mode": ParseMode.HTML}

View file

@ -14,4 +14,4 @@ class MyFilter(Filter):
@router.message(MyFilter("hello"))
async def my_handler(message: Message): ...
async def my_handler(message: Message) -> None: ...

View file

@ -263,7 +263,7 @@ quiz_router.message.register(QuizScene.as_handler(), Command("quiz"))
@quiz_router.message(Command("start"))
async def command_start(message: Message, scenes: ScenesManager):
async def command_start(message: Message, scenes: ScenesManager) -> None:
await scenes.close()
await message.answer(
"Hi! This is a quiz bot. To start the quiz, use the /quiz command.",
@ -271,7 +271,7 @@ async def command_start(message: Message, scenes: ScenesManager):
)
def create_dispatcher():
def create_dispatcher() -> Dispatcher:
# Event isolation is needed to correctly handle fast user responses
dispatcher = Dispatcher(
events_isolation=SimpleEventIsolation(),
@ -288,7 +288,7 @@ def create_dispatcher():
return dispatcher
async def main():
async def main() -> None:
dp = create_dispatcher()
bot = Bot(token=TOKEN)
await dp.start_polling(bot)

View file

@ -34,11 +34,11 @@ class CancellableScene(Scene):
"""
@on.message(F.text.casefold() == BUTTON_CANCEL.text.casefold(), after=After.exit())
async def handle_cancel(self, message: Message):
async def handle_cancel(self, message: Message) -> None:
await message.answer("Cancelled.", reply_markup=ReplyKeyboardRemove())
@on.message(F.text.casefold() == BUTTON_BACK.text.casefold(), after=After.back())
async def handle_back(self, message: Message):
async def handle_back(self, message: Message) -> None:
await message.answer("Back.")
@ -48,7 +48,7 @@ class LanguageScene(CancellableScene, state="language"):
"""
@on.message.enter()
async def on_enter(self, message: Message):
async def on_enter(self, message: Message) -> None:
await message.answer(
"What language do you prefer?",
reply_markup=ReplyKeyboardMarkup(
@ -58,14 +58,14 @@ class LanguageScene(CancellableScene, state="language"):
)
@on.message(F.text.casefold() == "python", after=After.exit())
async def process_python(self, message: Message):
async def process_python(self, message: Message) -> None:
await message.answer(
"Python, you say? That's the language that makes my circuits light up! 😉"
"Python, you say? That's the language that makes my circuits light up! 😉",
)
await self.input_language(message)
@on.message(after=After.exit())
async def input_language(self, message: Message):
async def input_language(self, message: Message) -> None:
data: FSMData = await self.wizard.get_data()
await self.show_results(message, language=message.text, **data)
@ -83,7 +83,7 @@ class LikeBotsScene(CancellableScene, state="like_bots"):
"""
@on.message.enter()
async def on_enter(self, message: Message):
async def on_enter(self, message: Message) -> None:
await message.answer(
"Did you like to write bots?",
reply_markup=ReplyKeyboardMarkup(
@ -96,18 +96,18 @@ class LikeBotsScene(CancellableScene, state="like_bots"):
)
@on.message(F.text.casefold() == "yes", after=After.goto(LanguageScene))
async def process_like_write_bots(self, message: Message):
async def process_like_write_bots(self, message: Message) -> None:
await message.reply("Cool! I'm too!")
@on.message(F.text.casefold() == "no", after=After.exit())
async def process_dont_like_write_bots(self, message: Message):
async def process_dont_like_write_bots(self, message: Message) -> None:
await message.answer(
"Not bad not terrible.\nSee you soon.",
reply_markup=ReplyKeyboardRemove(),
)
@on.message()
async def input_like_bots(self, message: Message):
async def input_like_bots(self, message: Message) -> None:
await message.answer("I don't understand you :(")
@ -117,25 +117,25 @@ class NameScene(CancellableScene, state="name"):
"""
@on.message.enter() # Marker for handler that should be called when a user enters the scene.
async def on_enter(self, message: Message):
async def on_enter(self, message: Message) -> None:
await message.answer(
"Hi there! What's your name?",
reply_markup=ReplyKeyboardMarkup(keyboard=[[BUTTON_CANCEL]], resize_keyboard=True),
)
@on.callback_query.enter() # different types of updates that start the scene also supported.
async def on_enter_callback(self, callback_query: CallbackQuery):
async def on_enter_callback(self, callback_query: CallbackQuery) -> None:
await callback_query.answer()
await self.on_enter(callback_query.message)
@on.message.leave() # Marker for handler that should be called when a user leaves the scene.
async def on_leave(self, message: Message):
async def on_leave(self, message: Message) -> None:
data: FSMData = await self.wizard.get_data()
name = data.get("name", "Anonymous")
await message.answer(f"Nice to meet you, {html.quote(name)}!")
@on.message(after=After.goto(LikeBotsScene))
async def input_name(self, message: Message):
async def input_name(self, message: Message) -> None:
await self.wizard.update_data(name=message.text)
@ -154,22 +154,22 @@ class DefaultScene(
start_demo = on.message(F.text.casefold() == "demo", after=After.goto(NameScene))
@on.message(Command("demo"))
async def demo(self, message: Message):
async def demo(self, message: Message) -> None:
await message.answer(
"Demo started",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[[InlineKeyboardButton(text="Go to form", callback_data="start")]]
inline_keyboard=[[InlineKeyboardButton(text="Go to form", callback_data="start")]],
),
)
@on.callback_query(F.data == "start", after=After.goto(NameScene))
async def demo_callback(self, callback_query: CallbackQuery):
async def demo_callback(self, callback_query: CallbackQuery) -> None:
await callback_query.answer(cache_time=0)
await callback_query.message.delete_reply_markup()
@on.message.enter() # Mark that this handler should be called when a user enters the scene.
@on.message()
async def default_handler(self, message: Message):
async def default_handler(self, message: Message) -> None:
await message.answer(
"Start demo?\nYou can also start demo via command /demo",
reply_markup=ReplyKeyboardMarkup(

View file

@ -33,7 +33,7 @@ async def command_start_handler(message: Message) -> None:
await message.answer(
f"Hello, {hbold(message.from_user.full_name)}!",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[[InlineKeyboardButton(text="Tap me, bro", callback_data="*")]]
inline_keyboard=[[InlineKeyboardButton(text="Tap me, bro", callback_data="*")]],
),
)
@ -43,7 +43,7 @@ async def chat_member_update(chat_member: ChatMemberUpdated, bot: Bot) -> None:
await bot.send_message(
chat_member.chat.id,
f"Member {hcode(chat_member.from_user.id)} was changed "
+ f"from {chat_member.old_chat_member.status} to {chat_member.new_chat_member.status}",
f"from {chat_member.old_chat_member.status} to {chat_member.new_chat_member.status}",
)

View file

@ -12,7 +12,7 @@ my_router = Router()
@my_router.message(CommandStart())
async def command_start(message: Message, bot: Bot, base_url: str):
async def command_start(message: Message, bot: Bot, base_url: str) -> None:
await bot.set_chat_menu_button(
chat_id=message.chat.id,
menu_button=MenuButtonWebApp(text="Open Menu", web_app=WebAppInfo(url=f"{base_url}/demo")),
@ -21,28 +21,29 @@ async def command_start(message: Message, bot: Bot, base_url: str):
@my_router.message(Command("webview"))
async def command_webview(message: Message, base_url: str):
async def command_webview(message: Message, base_url: str) -> None:
await message.answer(
"Good. Now you can try to send it via Webview",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[
InlineKeyboardButton(
text="Open Webview", web_app=WebAppInfo(url=f"{base_url}/demo")
)
]
]
text="Open Webview",
web_app=WebAppInfo(url=f"{base_url}/demo"),
),
],
],
),
)
@my_router.message(~F.message.via_bot) # Echo to all messages except messages via bot
async def echo_all(message: Message, base_url: str):
async def echo_all(message: Message, base_url: str) -> None:
await message.answer(
"Test webview",
reply_markup=InlineKeyboardMarkup(
inline_keyboard=[
[InlineKeyboardButton(text="Open", web_app=WebAppInfo(url=f"{base_url}/demo"))]
]
[InlineKeyboardButton(text="Open", web_app=WebAppInfo(url=f"{base_url}/demo"))],
],
),
)

View file

@ -18,14 +18,14 @@ TOKEN = getenv("BOT_TOKEN")
APP_BASE_URL = getenv("APP_BASE_URL")
async def on_startup(bot: Bot, base_url: str):
async def on_startup(bot: Bot, base_url: str) -> None:
await bot.set_webhook(f"{base_url}/webhook")
await bot.set_chat_menu_button(
menu_button=MenuButtonWebApp(text="Open Menu", web_app=WebAppInfo(url=f"{base_url}/demo"))
menu_button=MenuButtonWebApp(text="Open Menu", web_app=WebAppInfo(url=f"{base_url}/demo")),
)
def main():
def main() -> None:
bot = Bot(token=TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
dispatcher = Dispatcher()
dispatcher["base_url"] = APP_BASE_URL

View file

@ -1,10 +1,11 @@
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
from aiohttp.web_fileresponse import FileResponse
from aiohttp.web_request import Request
from aiohttp.web_response import json_response
from aiogram import Bot
from aiogram.types import (
InlineKeyboardButton,
InlineKeyboardMarkup,
@ -14,12 +15,18 @@ from aiogram.types import (
)
from aiogram.utils.web_app import check_webapp_signature, safe_parse_webapp_init_data
if TYPE_CHECKING:
from aiohttp.web_request import Request
from aiohttp.web_response import Response
async def demo_handler(request: Request):
from aiogram import Bot
async def demo_handler(request: Request) -> FileResponse:
return FileResponse(Path(__file__).parent.resolve() / "demo.html")
async def check_data_handler(request: Request):
async def check_data_handler(request: Request) -> Response:
bot: Bot = request.app["bot"]
data = await request.post()
@ -28,7 +35,7 @@ async def check_data_handler(request: Request):
return json_response({"ok": False, "err": "Unauthorized"}, status=401)
async def send_message_handler(request: Request):
async def send_message_handler(request: Request) -> Response:
bot: Bot = request.app["bot"]
data = await request.post()
try:
@ -44,11 +51,11 @@ async def send_message_handler(request: Request):
InlineKeyboardButton(
text="Open",
web_app=WebAppInfo(
url=str(request.url.with_scheme("https").with_path("demo"))
url=str(request.url.with_scheme("https").with_path("demo")),
),
)
]
]
),
],
],
)
await bot.answer_web_app_query(
web_app_query_id=web_app_init_data.query_id,

View file

@ -15,7 +15,7 @@ def create_parser() -> ArgumentParser:
return parser
async def main():
async def main() -> None:
parser = create_parser()
ns = parser.parse_args()