diff --git a/CHANGES/717.feature b/CHANGES/717.feature new file mode 100644 index 00000000..5fcadf91 --- /dev/null +++ b/CHANGES/717.feature @@ -0,0 +1,10 @@ +Improved description of filters resolving error. +For example when you try to pass wrong type of argument to the filter but don't know why filter is not resolved now you can get error like this: + +.. code-block:: python3 + + aiogram.exceptions.FiltersResolveError: Unknown keyword filters: {'content_types'} + Possible cases: + - 1 validation error for ContentTypesFilter + content_types + Invalid content types {'42'} is not allowed here (type=value_error) diff --git a/aiogram/dispatcher/event/telegram.py b/aiogram/dispatcher/event/telegram.py index 1d90d3d2..386d2fa4 100644 --- a/aiogram/dispatcher/event/telegram.py +++ b/aiogram/dispatcher/event/telegram.py @@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Optional from pydantic import ValidationError +from ...exceptions import FiltersResolveError from ...types import TelegramObject from ..filters.base import BaseFilter from .bases import ( @@ -108,11 +109,13 @@ class TelegramEventObserver: if not full_config: return filters + validation_errors = [] for bound_filter in self._resolve_filters_chain(): # Try to initialize filter. try: f = bound_filter(**full_config) - except ValidationError: + except ValidationError as e: + validation_errors.append(e) continue # Clean full config to prevent to re-initialize another filter @@ -123,7 +126,16 @@ class TelegramEventObserver: filters.append(f) if full_config: - raise ValueError(f"Unknown keyword filters: {set(full_config.keys())}") + possible_cases = [] + for error in validation_errors: + for sum_error in error.errors(): + if sum_error["loc"][0] in full_config: + possible_cases.append(error) + break + + raise FiltersResolveError( + unresolved_fields=set(full_config.keys()), possible_cases=possible_cases + ) return filters diff --git a/aiogram/exceptions.py b/aiogram/exceptions.py index 6f60fa64..514d7382 100644 --- a/aiogram/exceptions.py +++ b/aiogram/exceptions.py @@ -1,28 +1,37 @@ -from typing import Optional +from textwrap import indent +from typing import List, Optional, Set + +from pydantic import ValidationError from aiogram.methods import TelegramMethod from aiogram.methods.base import TelegramType -class TelegramAPIError(Exception): +class AiogramError(Exception): + pass + + +class DetailedAiogramError(AiogramError): url: Optional[str] = None + def __init__(self, message: str) -> None: + self.message = message + + def __str__(self) -> str: + message = self.message + if self.url: + message += f"\n(background on this error at: {self.url})" + return message + + +class TelegramAPIError(DetailedAiogramError): def __init__( self, method: TelegramMethod[TelegramType], message: str, ) -> None: + super().__init__(message=message) self.method = method - self.message = message - - def render_description(self) -> str: - return self.message - - def __str__(self) -> str: - message = [self.render_description()] - if self.url: - message.append(f"(background on this error at: {self.url})") - return "\n".join(message) class TelegramNetworkError(TelegramAPIError): @@ -38,15 +47,14 @@ class TelegramRetryAfter(TelegramAPIError): message: str, retry_after: int, ) -> None: - super().__init__(method=method, message=message) - self.retry_after = retry_after - - def render_description(self) -> str: - description = f"Flood control exceeded on method {type(self.method).__name__!r}" - if chat_id := getattr(self.method, "chat_id", None): + description = f"Flood control exceeded on method {type(method).__name__!r}" + if chat_id := getattr(method, "chat_id", None): description += f" in chat {chat_id}" - description += f". Retry in {self.retry_after} seconds." - return description + description += f". Retry in {retry_after} seconds." + description += f"\nOriginal description: {message}" + + super().__init__(method=method, message=description) + self.retry_after = retry_after class TelegramMigrateToChat(TelegramAPIError): @@ -58,17 +66,13 @@ class TelegramMigrateToChat(TelegramAPIError): message: str, migrate_to_chat_id: int, ) -> None: + description = f"The group has been migrated to a supergroup with id {migrate_to_chat_id}" + if chat_id := getattr(method, "chat_id", None): + description += f" from {chat_id}" + description += f"\nOriginal description: {message}" super().__init__(method=method, message=message) self.migrate_to_chat_id = migrate_to_chat_id - def render_description(self) -> str: - description = ( - f"The group has been migrated to a supergroup with id {self.migrate_to_chat_id}" - ) - if chat_id := getattr(self.method, "chat_id", None): - description += f" from {chat_id}" - return description - class TelegramBadRequest(TelegramAPIError): pass @@ -100,3 +104,17 @@ class RestartingTelegram(TelegramServerError): class TelegramEntityTooLarge(TelegramNetworkError): url = "https://core.telegram.org/bots/api#sending-files" + + +class FiltersResolveError(DetailedAiogramError): + def __init__(self, unresolved_fields: Set[str], possible_cases: List[ValidationError]) -> None: + possible_cases_str = "\n".join( + " - " + indent(str(e), " " * 4).lstrip() for e in possible_cases + ) + message = f"Unknown keyword filters: {unresolved_fields}" + if possible_cases_str: + message += f"\n Possible cases:\n{possible_cases_str}" + + super().__init__(message=message) + self.unresolved_fields = unresolved_fields + self.possible_cases = possible_cases diff --git a/tests/test_dispatcher/test_event/test_telegram.py b/tests/test_dispatcher/test_event/test_telegram.py index 39535219..563ffa9e 100644 --- a/tests/test_dispatcher/test_event/test_telegram.py +++ b/tests/test_dispatcher/test_event/test_telegram.py @@ -9,6 +9,7 @@ from aiogram.dispatcher.event.handler import HandlerObject from aiogram.dispatcher.event.telegram import TelegramEventObserver from aiogram.dispatcher.filters.base import BaseFilter from aiogram.dispatcher.router import Router +from aiogram.exceptions import FiltersResolveError from aiogram.types import Chat, Message, User pytestmark = pytest.mark.asyncio @@ -94,15 +95,15 @@ class TestTelegramEventObserver: assert any(isinstance(item, MyFilter1) for item in resolved) # Unknown filter - with pytest.raises(ValueError, match="Unknown keyword filters: {'@bad'}"): + with pytest.raises(FiltersResolveError, match="Unknown keyword filters: {'@bad'}"): assert observer.resolve_filters({"@bad": "very"}) # Unknown filter - with pytest.raises(ValueError, match="Unknown keyword filters: {'@bad'}"): + with pytest.raises(FiltersResolveError, match="Unknown keyword filters: {'@bad'}"): assert observer.resolve_filters({"test": "ok", "@bad": "very"}) # Bad argument type - with pytest.raises(ValueError, match="Unknown keyword filters: {'test'}"): + with pytest.raises(FiltersResolveError, match="Unknown keyword filters: {'test'}"): assert observer.resolve_filters({"test": ...}) def test_register(self):