Improve filters factory resolve error (#718)

This commit is contained in:
Alex Root Junior 2021-10-06 00:10:46 +03:00 committed by GitHub
parent 275bd509a1
commit 45a1fb2749
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 74 additions and 33 deletions

10
CHANGES/717.feature Normal file
View file

@ -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)

View file

@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, Generator, List, Optional
from pydantic import ValidationError from pydantic import ValidationError
from ...exceptions import FiltersResolveError
from ...types import TelegramObject from ...types import TelegramObject
from ..filters.base import BaseFilter from ..filters.base import BaseFilter
from .bases import ( from .bases import (
@ -108,11 +109,13 @@ class TelegramEventObserver:
if not full_config: if not full_config:
return filters return filters
validation_errors = []
for bound_filter in self._resolve_filters_chain(): for bound_filter in self._resolve_filters_chain():
# Try to initialize filter. # Try to initialize filter.
try: try:
f = bound_filter(**full_config) f = bound_filter(**full_config)
except ValidationError: except ValidationError as e:
validation_errors.append(e)
continue continue
# Clean full config to prevent to re-initialize another filter # Clean full config to prevent to re-initialize another filter
@ -123,7 +126,16 @@ class TelegramEventObserver:
filters.append(f) filters.append(f)
if full_config: 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 return filters

View file

@ -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 import TelegramMethod
from aiogram.methods.base import TelegramType from aiogram.methods.base import TelegramType
class TelegramAPIError(Exception): class AiogramError(Exception):
pass
class DetailedAiogramError(AiogramError):
url: Optional[str] = None 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__( def __init__(
self, self,
method: TelegramMethod[TelegramType], method: TelegramMethod[TelegramType],
message: str, message: str,
) -> None: ) -> None:
super().__init__(message=message)
self.method = method 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): class TelegramNetworkError(TelegramAPIError):
@ -38,15 +47,14 @@ class TelegramRetryAfter(TelegramAPIError):
message: str, message: str,
retry_after: int, retry_after: int,
) -> None: ) -> None:
super().__init__(method=method, message=message) description = f"Flood control exceeded on method {type(method).__name__!r}"
self.retry_after = retry_after if chat_id := getattr(method, "chat_id", None):
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" in chat {chat_id}" description += f" in chat {chat_id}"
description += f". Retry in {self.retry_after} seconds." description += f". Retry in {retry_after} seconds."
return description description += f"\nOriginal description: {message}"
super().__init__(method=method, message=description)
self.retry_after = retry_after
class TelegramMigrateToChat(TelegramAPIError): class TelegramMigrateToChat(TelegramAPIError):
@ -58,17 +66,13 @@ class TelegramMigrateToChat(TelegramAPIError):
message: str, message: str,
migrate_to_chat_id: int, migrate_to_chat_id: int,
) -> None: ) -> 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) super().__init__(method=method, message=message)
self.migrate_to_chat_id = migrate_to_chat_id 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): class TelegramBadRequest(TelegramAPIError):
pass pass
@ -100,3 +104,17 @@ class RestartingTelegram(TelegramServerError):
class TelegramEntityTooLarge(TelegramNetworkError): class TelegramEntityTooLarge(TelegramNetworkError):
url = "https://core.telegram.org/bots/api#sending-files" 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

View file

@ -9,6 +9,7 @@ from aiogram.dispatcher.event.handler import HandlerObject
from aiogram.dispatcher.event.telegram import TelegramEventObserver from aiogram.dispatcher.event.telegram import TelegramEventObserver
from aiogram.dispatcher.filters.base import BaseFilter from aiogram.dispatcher.filters.base import BaseFilter
from aiogram.dispatcher.router import Router from aiogram.dispatcher.router import Router
from aiogram.exceptions import FiltersResolveError
from aiogram.types import Chat, Message, User from aiogram.types import Chat, Message, User
pytestmark = pytest.mark.asyncio pytestmark = pytest.mark.asyncio
@ -94,15 +95,15 @@ class TestTelegramEventObserver:
assert any(isinstance(item, MyFilter1) for item in resolved) assert any(isinstance(item, MyFilter1) for item in resolved)
# Unknown filter # 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"}) assert observer.resolve_filters({"@bad": "very"})
# Unknown filter # 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"}) assert observer.resolve_filters({"test": "ok", "@bad": "very"})
# Bad argument type # 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": ...}) assert observer.resolve_filters({"test": ...})
def test_register(self): def test_register(self):