mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
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>
175 lines
5.5 KiB
Python
175 lines
5.5 KiB
Python
import inspect
|
|
from collections.abc import Iterator
|
|
from typing import Any, no_type_check
|
|
|
|
from aiogram.types import TelegramObject
|
|
|
|
|
|
class State:
|
|
"""
|
|
State object
|
|
"""
|
|
|
|
def __init__(self, state: str | None = None, group_name: str | None = None) -> None:
|
|
self._state = state
|
|
self._group_name = group_name
|
|
self._group: type[StatesGroup] | None = None
|
|
|
|
@property
|
|
def group(self) -> "type[StatesGroup]":
|
|
if not self._group:
|
|
msg = "This state is not in any group."
|
|
raise RuntimeError(msg)
|
|
return self._group
|
|
|
|
@property
|
|
def state(self) -> str | None:
|
|
if self._state is None or self._state == "*":
|
|
return self._state
|
|
|
|
if self._group_name is None and self._group:
|
|
group = self._group.__full_group_name__
|
|
elif self._group_name:
|
|
group = self._group_name
|
|
else:
|
|
group = "@"
|
|
|
|
return f"{group}:{self._state}"
|
|
|
|
def set_parent(self, group: "type[StatesGroup]") -> None:
|
|
if not issubclass(group, StatesGroup):
|
|
msg = "Group must be subclass of StatesGroup"
|
|
raise ValueError(msg)
|
|
self._group = group
|
|
|
|
def __set_name__(self, owner: "type[StatesGroup]", name: str) -> None:
|
|
if self._state is None:
|
|
self._state = name
|
|
self.set_parent(owner)
|
|
|
|
def __str__(self) -> str:
|
|
return f"<State '{self.state or ''}'>"
|
|
|
|
__repr__ = __str__
|
|
|
|
def __call__(self, event: TelegramObject, raw_state: str | None = None) -> bool:
|
|
if self.state == "*":
|
|
return True
|
|
return raw_state == self.state
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
if isinstance(other, self.__class__):
|
|
return self.state == other.state
|
|
if isinstance(other, str):
|
|
return self.state == other
|
|
return NotImplemented
|
|
|
|
def __hash__(self) -> int:
|
|
return hash(self.state)
|
|
|
|
|
|
class StatesGroupMeta(type):
|
|
__parent__: type["StatesGroup"] | None
|
|
__childs__: tuple[type["StatesGroup"], ...]
|
|
__states__: tuple[State, ...]
|
|
__state_names__: tuple[str, ...]
|
|
__all_childs__: tuple[type["StatesGroup"], ...]
|
|
__all_states__: tuple[State, ...]
|
|
__all_states_names__: tuple[str, ...]
|
|
|
|
@no_type_check
|
|
def __new__(mcs, name, bases, namespace, **kwargs):
|
|
cls = super().__new__(mcs, name, bases, namespace)
|
|
|
|
states = []
|
|
childs = []
|
|
|
|
for arg in namespace.values():
|
|
if isinstance(arg, State):
|
|
states.append(arg)
|
|
elif inspect.isclass(arg) and issubclass(arg, StatesGroup):
|
|
child = cls._prepare_child(arg)
|
|
childs.append(child)
|
|
|
|
cls.__parent__ = None
|
|
cls.__childs__ = tuple(childs)
|
|
cls.__states__ = tuple(states)
|
|
cls.__state_names__ = tuple(state.state for state in states)
|
|
|
|
cls.__all_childs__ = cls._get_all_childs()
|
|
cls.__all_states__ = cls._get_all_states()
|
|
|
|
# In order to ensure performance, we calculate this parameter
|
|
# in advance already during the production of the class.
|
|
# Depending on the relationship, it should be recalculated
|
|
cls.__all_states_names__ = cls._get_all_states_names()
|
|
|
|
return cls
|
|
|
|
@property
|
|
def __full_group_name__(cls) -> str:
|
|
if cls.__parent__:
|
|
return f"{cls.__parent__.__full_group_name__}.{cls.__name__}"
|
|
return cls.__name__
|
|
|
|
def _prepare_child(cls, child: type["StatesGroup"]) -> type["StatesGroup"]:
|
|
"""Prepare child.
|
|
|
|
While adding `cls` for its children, we also need to recalculate
|
|
the parameter `__all_states_names__` for each child
|
|
`StatesGroup`. Since the child class appears before the
|
|
parent, at the time of adding the parent, the child's
|
|
`__all_states_names__` is already recorded without taking into
|
|
account the name of current parent.
|
|
"""
|
|
child.__parent__ = cls # type: ignore[assignment]
|
|
child.__all_states_names__ = child._get_all_states_names()
|
|
return child
|
|
|
|
def _get_all_childs(cls) -> tuple[type["StatesGroup"], ...]:
|
|
result = cls.__childs__
|
|
for child in cls.__childs__:
|
|
result += child.__childs__
|
|
return result
|
|
|
|
def _get_all_states(cls) -> tuple[State, ...]:
|
|
result = cls.__states__
|
|
for group in cls.__childs__:
|
|
result += group.__all_states__
|
|
return result
|
|
|
|
def _get_all_states_names(cls) -> tuple[str, ...]:
|
|
return tuple(state.state for state in cls.__all_states__ if state.state)
|
|
|
|
def __contains__(cls, item: Any) -> bool:
|
|
if isinstance(item, str):
|
|
return item in cls.__all_states_names__
|
|
if isinstance(item, State):
|
|
return item in cls.__all_states__
|
|
if isinstance(item, StatesGroupMeta):
|
|
return item in cls.__all_childs__
|
|
return False
|
|
|
|
def __str__(self) -> str:
|
|
return f"<StatesGroup '{self.__full_group_name__}'>"
|
|
|
|
def __iter__(self) -> Iterator[State]:
|
|
return iter(self.__all_states__)
|
|
|
|
|
|
class StatesGroup(metaclass=StatesGroupMeta):
|
|
@classmethod
|
|
def get_root(cls) -> type["StatesGroup"]:
|
|
if cls.__parent__ is None:
|
|
return cls
|
|
return cls.__parent__.get_root()
|
|
|
|
def __call__(self, event: TelegramObject, raw_state: str | None = None) -> bool:
|
|
return raw_state in type(self).__all_states_names__
|
|
|
|
def __str__(self) -> str:
|
|
return f"StatesGroup {type(self).__full_group_name__}"
|
|
|
|
|
|
default_state = State()
|
|
any_state = State(state="*")
|