Refactor FSM Storage methods input types to calm down MyPy. (#1683)
Some checks failed
Tests / tests (windows-latest, 3.13) (push) Has been cancelled
Tests / tests (windows-latest, 3.9) (push) Has been cancelled
Tests / pypy-tests (macos-latest, pypy3.10) (push) Has been cancelled
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 (macos-latest, 3.9) (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 (ubuntu-latest, 3.9) (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 / pypy-tests (macos-latest, pypy3.9) (push) Has been cancelled
Tests / pypy-tests (ubuntu-latest, pypy3.10) (push) Has been cancelled
Tests / pypy-tests (ubuntu-latest, pypy3.9) (push) Has been cancelled

* Refactor methods input types to calm down MyPy.
- `FSMContext.set_data`
- `FSMContext.update_data`
- `BaseStorage.set_data`
- `BaseStorage.update_data`
- `BaseStorage`'s child methods
- `SceneWizard.set_data`
- `SceneWizard.update_data`

* Add 1683.feature.rst

* Remove re-init in `DataNotDictLikeError`
This commit is contained in:
Andrew 2025-05-17 00:37:05 +03:00 committed by GitHub
parent afecf00f4a
commit e011d103c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 99 additions and 18 deletions

11
CHANGES/1683.feature.rst Normal file
View file

@ -0,0 +1,11 @@
Refactor methods input types to calm down MyPy. #1682
`Dict[str, Any]` is replaced with `Mapping[str, Any]` in the following methods:
- `FSMContext.set_data`
- `FSMContext.update_data`
- `BaseStorage.set_data`
- `BaseStorage.update_data`
- `BaseStorage's child methods`
- `SceneWizard.set_data`
- `SceneWizard.update_data`

View file

@ -197,3 +197,9 @@ class ClientDecodeError(AiogramError):
f"{original_type.__module__}.{original_type.__name__}: {self.original}\n"
f"Content: {self.data}"
)
class DataNotDictLikeError(DetailedAiogramError):
"""
Exception raised when data is not dict-like.
"""

View file

@ -1,4 +1,4 @@
from typing import Any, Dict, Optional, overload
from typing import Any, Dict, Mapping, Optional, overload
from aiogram.fsm.storage.base import BaseStorage, StateType, StorageKey
@ -14,7 +14,7 @@ class FSMContext:
async def get_state(self) -> Optional[str]:
return await self.storage.get_state(key=self.key)
async def set_data(self, data: Dict[str, Any]) -> None:
async def set_data(self, data: Mapping[str, Any]) -> None:
await self.storage.set_data(key=self.key, data=data)
async def get_data(self) -> Dict[str, Any]:
@ -30,7 +30,7 @@ class FSMContext:
return await self.storage.get_value(storage_key=self.key, dict_key=key, default=default)
async def update_data(
self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
self, data: Optional[Mapping[str, Any]] = None, **kwargs: Any
) -> Dict[str, Any]:
if data:
kwargs.update(data)

View file

@ -4,7 +4,18 @@ import inspect
from collections import defaultdict
from dataclasses import dataclass, replace
from enum import Enum, auto
from typing import Any, ClassVar, Dict, List, Optional, Tuple, Type, Union, overload
from typing import (
Any,
ClassVar,
Dict,
List,
Mapping,
Optional,
Tuple,
Type,
Union,
overload,
)
from typing_extensions import Self
@ -577,11 +588,11 @@ class SceneWizard:
await action_config[event_type].call(self.scene, self.event, **{**self.data, **kwargs})
return True
async def set_data(self, data: Dict[str, Any]) -> None:
async def set_data(self, data: Mapping[str, Any]) -> None:
"""
Sets custom data in the current state.
:param data: A dictionary containing the custom data to be set in the current state.
:param data: A mapping containing the custom data to be set in the current state.
:return: None
"""
await self.state.set_data(data=data)
@ -621,12 +632,12 @@ class SceneWizard:
return await self.state.get_value(key, default)
async def update_data(
self, data: Optional[Dict[str, Any]] = None, **kwargs: Any
self, data: Optional[Mapping[str, Any]] = None, **kwargs: Any
) -> Dict[str, Any]:
"""
This method updates the data stored in the current state
:param data: Optional dictionary of data to update.
:param data: Optional mapping of data to update.
:param kwargs: Additional key-value pairs of data to update.
:return: Dictionary of updated data
"""

View file

@ -1,7 +1,16 @@
from abc import ABC, abstractmethod
from contextlib import asynccontextmanager
from dataclasses import dataclass
from typing import Any, AsyncGenerator, Dict, Literal, Optional, Union, overload
from typing import (
Any,
AsyncGenerator,
Dict,
Literal,
Mapping,
Optional,
Union,
overload,
)
from aiogram.fsm.state import State
@ -125,7 +134,7 @@ class BaseStorage(ABC):
pass
@abstractmethod
async def set_data(self, key: StorageKey, data: Dict[str, Any]) -> None:
async def set_data(self, key: StorageKey, data: Mapping[str, Any]) -> None:
"""
Write data (replace)
@ -173,7 +182,7 @@ class BaseStorage(ABC):
data = await self.get_data(storage_key)
return data.get(dict_key, default)
async def update_data(self, key: StorageKey, data: Dict[str, Any]) -> Dict[str, Any]:
async def update_data(self, key: StorageKey, data: Mapping[str, Any]) -> Dict[str, Any]:
"""
Update date in the storage for key (like dict.update)

View file

@ -3,8 +3,18 @@ from collections import defaultdict
from contextlib import asynccontextmanager
from copy import copy
from dataclasses import dataclass, field
from typing import Any, AsyncGenerator, DefaultDict, Dict, Hashable, Optional, overload
from typing import (
Any,
AsyncGenerator,
DefaultDict,
Dict,
Hashable,
Mapping,
Optional,
overload,
)
from aiogram.exceptions import DataNotDictLikeError
from aiogram.fsm.state import State
from aiogram.fsm.storage.base import (
BaseEventIsolation,
@ -44,7 +54,11 @@ class MemoryStorage(BaseStorage):
async def get_state(self, key: StorageKey) -> Optional[str]:
return self.storage[key].state
async def set_data(self, key: StorageKey, data: Dict[str, Any]) -> None:
async def set_data(self, key: StorageKey, data: Mapping[str, Any]) -> None:
if not isinstance(data, dict):
raise DataNotDictLikeError(
f"Data must be a dict or dict-like object, got {type(data).__name__}"
)
self.storage[key].data = data.copy()
async def get_data(self, key: StorageKey) -> Dict[str, Any]:

View file

@ -1,7 +1,8 @@
from typing import Any, Dict, Optional, cast
from typing import Any, Dict, Mapping, Optional, cast
from motor.motor_asyncio import AsyncIOMotorClient
from aiogram.exceptions import DataNotDictLikeError
from aiogram.fsm.state import State
from aiogram.fsm.storage.base import (
BaseStorage,
@ -90,7 +91,12 @@ class MongoStorage(BaseStorage):
return None
return document.get("state")
async def set_data(self, key: StorageKey, data: Dict[str, Any]) -> None:
async def set_data(self, key: StorageKey, data: Mapping[str, Any]) -> None:
if not isinstance(data, dict):
raise DataNotDictLikeError(
f"Data must be a dict or dict-like object, got {type(data).__name__}"
)
document_id = self._key_builder.build(key)
if not data:
updated = await self._collection.find_one_and_update(
@ -115,7 +121,7 @@ class MongoStorage(BaseStorage):
return {}
return cast(Dict[str, Any], document["data"])
async def update_data(self, key: StorageKey, data: Dict[str, Any]) -> Dict[str, Any]:
async def update_data(self, key: StorageKey, data: Mapping[str, Any]) -> Dict[str, Any]:
document_id = self._key_builder.build(key)
update_with = {f"data.{key}": value for key, value in data.items()}
update_result = await self._collection.find_one_and_update(

View file

@ -1,12 +1,13 @@
import json
from contextlib import asynccontextmanager
from typing import Any, AsyncGenerator, Callable, Dict, Optional, cast
from typing import Any, AsyncGenerator, Callable, Dict, Mapping, Optional, cast
from redis.asyncio.client import Redis
from redis.asyncio.connection import ConnectionPool
from redis.asyncio.lock import Lock
from redis.typing import ExpiryT
from aiogram.exceptions import DataNotDictLikeError
from aiogram.fsm.state import State
from aiogram.fsm.storage.base import (
BaseEventIsolation,
@ -103,8 +104,13 @@ class RedisStorage(BaseStorage):
async def set_data(
self,
key: StorageKey,
data: Dict[str, Any],
data: Mapping[str, Any],
) -> None:
if not isinstance(data, dict):
raise DataNotDictLikeError(
f"Data must be a dict or dict-like object, got {type(data).__name__}"
)
redis_key = self.key_builder.build(key, "data")
if not data:
await self.redis.delete(redis_key)

View file

@ -1,5 +1,8 @@
from typing import TypedDict
import pytest
from aiogram.exceptions import DataNotDictLikeError
from aiogram.fsm.storage.base import BaseStorage, StorageKey
@ -44,6 +47,21 @@ class TestStorages:
== "baz"
)
class CustomTypedDict(TypedDict, total=False):
foo: str
bar: str
await storage.set_data(key=storage_key, data=CustomTypedDict(foo="bar", bar="baz"))
assert await storage.get_data(key=storage_key) == {"foo": "bar", "bar": "baz"}
assert await storage.get_value(storage_key=storage_key, dict_key="foo") == "bar"
assert (
await storage.get_value(storage_key=storage_key, dict_key="foo", default="baz")
== "bar"
)
with pytest.raises(DataNotDictLikeError):
await storage.set_data(key=storage_key, data=())
async def test_update_data(self, storage: BaseStorage, storage_key: StorageKey):
assert await storage.get_data(key=storage_key) == {}
assert await storage.update_data(key=storage_key, data={"foo": "bar"}) == {"foo": "bar"}