Migrate to hatchling (#1095)

* Migrate to hatchling instead of poetry, ruff instead of flake8

* Migrate to hatchling instead of poetry, ruff instead of flake8

* Update tests suite

* venv?

* -m venv?

* Change dependencies

* Remove venv

* Change mypy config

* Added changelog

* Mark uvloop incompatible with pypy

* Update release script

* Use internal caching for dependencies

* Re-disable cov branches

* Added contributing guide
This commit is contained in:
Alex Root Junior 2023-01-12 02:49:58 +02:00 committed by GitHub
parent 04ccb390d5
commit f4ce4431f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 799 additions and 3001 deletions

View file

@ -1,6 +0,0 @@
[report]
exclude_lines =
pragma: no cover
if TYPE_CHECKING:
@abstractmethod
@overload

12
.flake8
View file

@ -1,12 +0,0 @@
[flake8]
max-line-length = 99
select = C,E,F,W,B,B950
ignore = E501,W503,E203
exclude =
.git
build
dist
venv
docs
*.egg-info
experiment.py

View file

@ -1,4 +1,4 @@
name: Deploy
name: "Deploy"
on:
push:
@ -17,16 +17,12 @@ jobs:
with:
python-version: "3.10"
- name: Install and configure Poetry
uses: snok/install-poetry@v1
with:
version: 1.2.1
virtualenvs-create: false
installer-parallel: true
- name: Install build dependencies
run: python -m pip install --upgrade build
- name: Build source distribution
run: python -m build .
- name: Build
run: |
poetry build
- name: Try install wheel
run: |
pip install -U virtualenv

View file

@ -8,11 +8,7 @@ on:
- ".github/workflows/tests.yml"
- "aiogram/**"
- "tests/**"
- ".coveragerc"
- ".flake8"
- "codecov.yaml"
- "mypy.ini"
- "poetry.lock"
- "pyproject.toml"
pull_request:
branches:
@ -21,11 +17,6 @@ on:
- ".github/workflows/tests.yml"
- "aiogram/**"
- "tests/**"
- ".coveragerc"
- ".flake8"
- "codecov.yaml"
- "mypy.ini"
- "poetry.lock"
- "pyproject.toml"
jobs:
@ -67,30 +58,25 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@master
uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
cache-dependency-path: pyproject.toml
- name: Install and configure Poetry
uses: snok/install-poetry@v1
if: "env.IS_PYPY == 'false' || env.IS_WINDOWS == 'false'"
with:
version: 1.2.1
virtualenvs-create: true
virtualenvs-in-project: true
installer-parallel: true
- name: Install and configure Poetry (PyPy on Windows)
if: "env.IS_PYPY == 'true' && env.IS_WINDOWS == 'true'"
- name: Install project dependencies
run: |
set -eu
pip install "poetry==1.2.1"
poetry config virtualenvs.create true
poetry config virtualenvs.in-project true
poetry config installer.parallel true
pip install -e .[dev,test,redis,proxy,i18n,fast]
- name: Lint code
if: "env.IS_PYPY == 'false'"
run: |
ruff --format=github aiogram examples
mypy aiogram
black --check --diff aiogram tests
- name: Setup redis
if: ${{ env.IS_WINDOWS == 'false' }}
@ -98,33 +84,12 @@ jobs:
with:
redis-version: 6
- name: Load cached venv
id: cached-poetry-dependencies
uses: actions/cache@v2
with:
path: .venv
key: venv-${{ matrix.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}-${{ secrets.CACHE_VERSION }}
- name: Install project dependencies
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
run: |
flags=""
[[ "$IS_PYPY" == "false" ]] && flags="$flags -E fast"
poetry install --no-interaction -E redis -E proxy -E i18n $flags
- name: Lint code
if: "env.IS_PYPY == 'false'"
run: |
poetry run flake8 aiogram
poetry run mypy aiogram
poetry run black --check --diff aiogram tests
- name: Run tests
run: |
flags=""
[[ "$IS_PYPY" == "false" ]] && flags="$flags --cov=aiogram --cov-config .coveragerc --cov-report=xml"
[[ "$IS_WINDOWS" == "false" ]] && flags="$flags --redis redis://localhost:6379/0"
poetry run pytest $flags
pytest $flags
- name: Upload coverage data
if: "env.IS_PYPY == 'false'"

2
.gitignore vendored
View file

@ -13,7 +13,7 @@ dist/
site/
*.egg-info/
*.egg
aiogram/_meta.py
.ruff_cache
.mypy_cache
.pytest_cache

View file

@ -19,34 +19,8 @@ repos:
- id: black
files: &files '^(aiogram|tests|examples)'
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.10.1
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: 'v0.0.215'
hooks:
- id: isort
additional_dependencies: [ toml ]
files: *files
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
hooks:
- id: flake8
args: [ '--config=.flake8' ]
files: *files
- repo: https://github.com/floatingpurr/sync_with_poetry
rev: 0.2.0
hooks:
- id: sync_with_poetry
- repo: https://github.com/python-poetry/poetry
rev: '1.2.1'
hooks:
- id: poetry-check
- id: poetry-lock
args: [ "--no-update" ]
- id: poetry-export
args: [ "-f", "requirements.txt", "--without-hashes", "-o", "requirements/base.txt" ]
- id: poetry-export
args: [ "-f", "requirements.txt", "--without-hashes", "-o", "requirements/docs.txt",
"-E", "fast", "-E", "redis", "-E", "proxy", "-E", "i18n",
"--with", "docs" ]
- id: ruff
args: [ "--force-exclude" ]

View file

@ -1,11 +1,13 @@
version: 2
python:
version: "3.8"
version: "3.10"
install:
- method: pip
path: .
- requirements: requirements/docs.txt
extra_requirements:
- docs
- redis
sphinx:
configuration: docs/conf.py

1
CHANGES/1095.misc.rst Normal file
View file

@ -0,0 +1 @@
Updated package metadata, moved build internals from Poetry to Hatch, added contributing guides.

1
CONTRIBUTING.md Normal file
View file

@ -0,0 +1 @@
Please read the [Contributing](https://docs.aiogram.dev/en/dev-3.x/contributing/) guidelines in the documentation site.

View file

@ -1,8 +1,4 @@
.DEFAULT_GOAL := help
base_python := python3
py := poetry run
python := $(py) python
.DEFAULT_GOAL := lint
package_dir := aiogram
tests_dir := tests
@ -13,43 +9,10 @@ reports_dir := reports
redis_connection := redis://localhost:6379
.PHONY: help
help:
@echo "======================================================================================="
@echo " aiogram build tools "
@echo "======================================================================================="
@echo "Environment:"
@echo " help: Show this message"
@echo " install: Install development dependencies"
@echo " clean: Delete temporary files"
@echo ""
@echo "Code quality:"
@echo " lint: Lint code by isort, black, flake8 and mypy tools"
@echo " reformat: Reformat code by isort and black tools"
@echo ""
@echo "Tests:"
@echo " test: Run tests"
@echo " test-coverage: Run tests with HTML reporting (results + coverage)"
@echo " test-coverage-report: Open coverage report in default system web browser"
@echo ""
@echo "Documentation:"
@echo " docs: Build docs"
@echo " docs-serve: Serve docs for local development"
@echo " docs-prepare-reports: Move all HTML reports to docs dir"
@echo ""
@echo "Project"
@echo " build: Run tests build package and docs"
@echo ""
# =================================================================================================
# Environment
# =================================================================================================
.PHONY: install
install:
poetry install --all-extras
$(py) pre-commit install
.PHONY: clean
clean:
rm -rf `find . -name __pycache__`
@ -68,16 +31,15 @@ clean:
.PHONY: lint
lint:
$(py) isort --check-only $(code_dir)
$(py) black --check --diff $(code_dir)
$(py) flake8 $(code_dir)
$(py) mypy $(package_dir)
# TODO: wemake-python-styleguide
isort --check-only $(code_dir)
black --check --diff $(code_dir)
ruff $(package_dir)
mypy $(package_dir)
.PHONY: reformat
reformat:
$(py) black $(code_dir)
$(py) isort $(code_dir)
black $(code_dir)
isort $(code_dir)
# =================================================================================================
# Tests
@ -88,17 +50,17 @@ test-run-services:
.PHONY: test
test: test-run-services
$(py) pytest --cov=aiogram --cov-config .coveragerc tests/ --redis $(redis_connection)
pytest --cov=aiogram --cov-config .coveragerc tests/ --redis $(redis_connection)
.PHONY: test-coverage
test-coverage: test-run-services
mkdir -p $(reports_dir)/tests/
$(py) pytest --cov=aiogram --cov-config .coveragerc --html=$(reports_dir)/tests/index.html tests/ --redis $(redis_connection)
$(py) coverage html -d $(reports_dir)/coverage
pytest --cov=aiogram --cov-config .coveragerc --html=$(reports_dir)/tests/index.html tests/ --redis $(redis_connection)
coverage html -d $(reports_dir)/coverage
.PHONY: test-coverage-view
test-coverage-view:
$(py) coverage html -d $(reports_dir)/coverage
coverage html -d $(reports_dir)/coverage
python -c "import webbrowser; webbrowser.open('file://$(shell pwd)/reports/coverage/index.html')"
# =================================================================================================
@ -117,7 +79,7 @@ docs-gettext:
docs-serve:
#rm -rf docs/_build
$(py) sphinx-autobuild --watch aiogram/ --watch CHANGELOG.rst --watch README.rst docs/ docs/_build/ $(OPTS)
sphinx-autobuild --watch aiogram/ --watch CHANGELOG.rst --watch README.rst docs/ docs/_build/ $(OPTS)
.PHONY: docs-serve
$(locale_targets): docs-serve-%:
@ -129,15 +91,13 @@ $(locale_targets): docs-serve-%:
# =================================================================================================
.PHONY: build
build: clean flake8-report mypy-report test-coverage
mkdir -p site/simple
poetry build
mv dist site/simple/aiogram
build: clean
hatch build
.PHONY: bump
bump:
poetry version $(args)
$(python) scripts/bump_versions.py
hatch version $(args)
python scripts/bump_versions.py
.PHONY: towncrier-build
towncrier-build:
@ -160,10 +120,3 @@ release:
git add .
git commit -m "Release $(shell poetry version -s)"
git tag v$(shell poetry version -s)
_poetry_export_args := --format requirements.txt --without-hashes
.PHONY: export-requirements
export-requirements:
poetry export $(_poetry_export_args) --output requirements/base.txt
poetry export $(_poetry_export_args) --output requirements/docs.txt -E fast -E redis -E proxy -E i18n --with docs

View file

@ -1,5 +1,9 @@
from aiogram.dispatcher.flags import FlagGenerator
from contextlib import suppress
from aiogram.dispatcher.flags import FlagGenerator
from . import methods
from . import types
from . import enums
from .client import session
from .client.bot import Bot
from .dispatcher.dispatcher import Dispatcher
@ -9,12 +13,10 @@ from .utils.magic_filter import MagicFilter
from .utils.text_decorations import html_decoration as html
from .utils.text_decorations import markdown_decoration as md
try:
with suppress(ImportError):
import uvloop as _uvloop
_uvloop.install()
except ImportError: # pragma: no cover
pass
F = MagicFilter()
flags = FlagGenerator()
@ -24,6 +26,7 @@ __all__ = (
"__version__",
"types",
"methods",
"enums",
"Bot",
"session",
"Dispatcher",

View file

@ -310,7 +310,6 @@ class Bot(ContextInstanceMixin["Bot"]):
if isinstance(destination, (str, pathlib.Path)):
await self.__download_file(destination=destination, stream=stream)
return None
else:
return await self.__download_file_binary_io(
destination=destination, seek=seek, stream=stream
)

View file

@ -48,18 +48,22 @@ def _retrieve_basic(basic: _ProxyBasic) -> Dict[str, Any]:
username = proxy_auth.login
password = proxy_auth.password
return dict(
proxy_type=proxy_type,
host=host,
port=port,
username=username,
password=password,
rdns=True,
)
return {
"proxy_type": proxy_type,
"host": host,
"port": port,
"username": username,
"password": password,
"rdns": True,
}
def _prepare_connector(chain_or_plain: _ProxyType) -> Tuple[Type["TCPConnector"], Dict[str, Any]]:
from aiohttp_socks import ChainProxyConnector, ProxyConnector, ProxyInfo # type: ignore
from aiohttp_socks import ( # type: ignore
ChainProxyConnector,
ProxyConnector,
ProxyInfo,
)
# since tuple is Iterable(compatible with _ProxyChain) object, we assume that
# user wants chained proxies if tuple is a pair of string(url) and BasicAuth
@ -74,7 +78,7 @@ def _prepare_connector(chain_or_plain: _ProxyType) -> Tuple[Type["TCPConnector"]
for basic in chain_or_plain:
infos.append(ProxyInfo(**_retrieve_basic(basic)))
return ChainProxyConnector, dict(proxy_infos=infos)
return ChainProxyConnector, {"proxy_infos": infos}
class AiohttpSession(BaseSession):

View file

@ -6,7 +6,17 @@ import json
from enum import Enum
from http import HTTPStatus
from types import TracebackType
from typing import TYPE_CHECKING, Any, AsyncGenerator, Callable, Final, Optional, Type, Union, cast
from typing import (
TYPE_CHECKING,
Any,
AsyncGenerator,
Callable,
Final,
Optional,
Type,
Union,
cast,
)
from pydantic import ValidationError
@ -165,7 +175,6 @@ class BaseSession(abc.ABC):
return str(round(value.timestamp()))
if isinstance(value, Enum):
return self.prepare_value(value.value)
else:
return str(value)
def clean_json(self, value: Any) -> Any:
@ -174,7 +183,7 @@ class BaseSession(abc.ABC):
"""
if isinstance(value, list):
return [self.clean_json(v) for v in value if v is not None]
elif isinstance(value, dict):
if isinstance(value, dict):
return {k: self.clean_json(v) for k, v in value.items() if v is not None}
return value

View file

@ -52,7 +52,8 @@ class TelegramAPIServer:
file: str
"""Files URL"""
is_local: bool = False
"""Mark this server is in `local mode <https://core.telegram.org/bots/api#using-a-local-bot-api-server>`_."""
"""Mark this server is
in `local mode <https://core.telegram.org/bots/api#using-a-local-bot-api-server>`_."""
wrap_local_file: FilesPathWrapper = BareFilesPathWrapper()
"""Callback to wrap files path in local mode"""

View file

@ -107,13 +107,13 @@ class Dispatcher(Router):
return self.fsm.storage
@property
def parent_router(self) -> None:
def parent_router(self) -> Optional[Router]:
"""
Dispatcher has no parent router and can't be included to any other routers or dispatchers
:return:
"""
return None
return None # noqa: RET501
@parent_router.setter
def parent_router(self, value: Router) -> None:

View file

@ -9,7 +9,8 @@ class EventObserver:
"""
Simple events observer
Is used for managing events is not related with Telegram (For example startup/shutdown processes)
Is used for managing events is not related with Telegram
(For example startup/shutdown processes)
Handlers can be registered via decorator or method

View file

@ -94,7 +94,7 @@ def extract_flags(handler: Union["HandlerObject", Dict[str, Any]]) -> Dict[str,
handler = handler["handler"]
if not hasattr(handler, "flags"):
return {}
return handler.flags # type: ignore
return handler.flags
def get_flag(

View file

@ -1,7 +1,11 @@
import functools
from typing import Any, Callable, Dict, List, Optional, Sequence, Union, overload
from aiogram.dispatcher.event.bases import MiddlewareEventType, MiddlewareType, NextMiddlewareType
from aiogram.dispatcher.event.bases import (
MiddlewareEventType,
MiddlewareType,
NextMiddlewareType,
)
from aiogram.dispatcher.event.handler import CallbackType
from aiogram.types import TelegramObject

View file

@ -101,7 +101,7 @@ class Router:
if observer.handlers and update_name not in skip_events:
handlers_in_use.add(update_name)
return list(sorted(handlers_in_use))
return list(sorted(handlers_in_use)) # NOQA: C413
async def propagate_event(self, update_type: str, event: TelegramObject, **kwargs: Any) -> Any:
kwargs.update(event_router=self)

View file

@ -120,6 +120,7 @@ class ClientDecodeError(AiogramError):
original_type = type(self.original)
return (
f"{self.message}\n"
f"Caused from error: {original_type.__module__}.{original_type.__name__}: {self.original}\n"
f"Caused from error: "
f"{original_type.__module__}.{original_type.__name__}: {self.original}\n"
f"Content: {self.data}"
)

View file

@ -37,7 +37,8 @@ class Filter(ABC):
def update_handler_flags(self, flags: Dict[str, Any]) -> None:
"""
Also if you want to extend handler flags with using this filter you should implement this method
Also if you want to extend handler flags with using this filter
you should implement this method
:param flags: existing flags, can be updated directly
"""

View file

@ -3,7 +3,17 @@ from __future__ import annotations
from decimal import Decimal
from enum import Enum
from fractions import Fraction
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Literal, Optional, Type, TypeVar, Union
from typing import (
TYPE_CHECKING,
Any,
ClassVar,
Dict,
Literal,
Optional,
Type,
TypeVar,
Union,
)
from uuid import UUID
from magic_filter import MagicFilter

View file

@ -22,7 +22,7 @@ class _MemberStatusMarker:
result = self.name.upper()
if self.is_member is not None:
result = ("+" if self.is_member else "-") + result
return result
return result # noqa: RET504
def __pos__(self: MarkerT) -> MarkerT:
return type(self)(name=self.name, is_member=True)
@ -38,7 +38,8 @@ class _MemberStatusMarker:
if isinstance(other, _MemberStatusGroupMarker):
return other | self
raise TypeError(
f"unsupported operand type(s) for |: {type(self).__name__!r} and {type(other).__name__!r}"
f"unsupported operand type(s) for |: "
f"{type(self).__name__!r} and {type(other).__name__!r}"
)
__ror__ = __or__
@ -52,7 +53,8 @@ class _MemberStatusMarker:
if isinstance(other, _MemberStatusGroupMarker):
return _MemberStatusTransition(old=old, new=other)
raise TypeError(
f"unsupported operand type(s) for >>: {type(self).__name__!r} and {type(other).__name__!r}"
f"unsupported operand type(s) for >>: "
f"{type(self).__name__!r} and {type(other).__name__!r}"
)
def __lshift__(
@ -64,7 +66,8 @@ class _MemberStatusMarker:
if isinstance(other, _MemberStatusGroupMarker):
return _MemberStatusTransition(old=other, new=new)
raise TypeError(
f"unsupported operand type(s) for <<: {type(self).__name__!r} and {type(other).__name__!r}"
f"unsupported operand type(s) for <<: "
f"{type(self).__name__!r} and {type(other).__name__!r}"
)
def __hash__(self) -> int:
@ -89,10 +92,11 @@ class _MemberStatusGroupMarker:
) -> MarkerGroupT:
if isinstance(other, _MemberStatusMarker):
return type(self)(*self.statuses, other)
elif isinstance(other, _MemberStatusGroupMarker):
if isinstance(other, _MemberStatusGroupMarker):
return type(self)(*self.statuses, *other.statuses)
raise TypeError(
f"unsupported operand type(s) for |: {type(self).__name__!r} and {type(other).__name__!r}"
f"unsupported operand type(s) for |: "
f"{type(self).__name__!r} and {type(other).__name__!r}"
)
def __rshift__(
@ -103,7 +107,8 @@ class _MemberStatusGroupMarker:
if isinstance(other, _MemberStatusGroupMarker):
return _MemberStatusTransition(old=self, new=other)
raise TypeError(
f"unsupported operand type(s) for >>: {type(self).__name__!r} and {type(other).__name__!r}"
f"unsupported operand type(s) for >>: "
f"{type(self).__name__!r} and {type(other).__name__!r}"
)
def __lshift__(
@ -114,7 +119,8 @@ class _MemberStatusGroupMarker:
if isinstance(other, _MemberStatusGroupMarker):
return _MemberStatusTransition(old=other, new=self)
raise TypeError(
f"unsupported operand type(s) for <<: {type(self).__name__!r} and {type(other).__name__!r}"
f"unsupported operand type(s) for <<: "
f"{type(self).__name__!r} and {type(other).__name__!r}"
)
def __str__(self) -> str:
@ -124,10 +130,7 @@ class _MemberStatusGroupMarker:
return result
def check(self, *, member: ChatMember) -> bool:
for status in self.statuses:
if status.check(member=member):
return True
return False
return any(status.check(member=member) for status in self.statuses)
class _MemberStatusTransition:

View file

@ -181,7 +181,7 @@ class Command(Filter):
await self.validate_mention(bot=bot, command=command)
command = self.validate_command(command)
command = self.do_magic(command=command)
return command
return command # noqa: RET504
def do_magic(self, command: CommandObject) -> Any:
if not self.magic:

View file

@ -84,7 +84,7 @@ class Text(Filter):
@classmethod
def _validate_constraints(cls, **values: Any) -> None:
# Validate that only one text filter type is presented
used_args = set(key for key, value in values.items() if value is not None)
used_args = {key for key, value in values.items() if value is not None}
if len(used_args) < 1:
raise ValueError(f"Filter should contain one of arguments: {set(values.keys())}")
if len(used_args) > 1:
@ -133,5 +133,4 @@ class Text(Filter):
def prepare_text(self, text: str) -> str:
if self.ignore_case:
return str(text).lower()
else:
return str(text)

View file

@ -3,7 +3,12 @@ from typing import Any, Awaitable, Callable, Dict, Optional, cast
from aiogram import Bot
from aiogram.dispatcher.middlewares.base import BaseMiddleware
from aiogram.fsm.context import FSMContext
from aiogram.fsm.storage.base import DEFAULT_DESTINY, BaseEventIsolation, BaseStorage, StorageKey
from aiogram.fsm.storage.base import (
DEFAULT_DESTINY,
BaseEventIsolation,
BaseStorage,
StorageKey,
)
from aiogram.fsm.strategy import FSMStrategy, apply_strategy
from aiogram.types import TelegramObject

View file

@ -6,7 +6,12 @@ from typing import Any, AsyncGenerator, DefaultDict, Dict, Hashable, Optional
from aiogram import Bot
from aiogram.fsm.state import State
from aiogram.fsm.storage.base import BaseEventIsolation, BaseStorage, StateType, StorageKey
from aiogram.fsm.storage.base import (
BaseEventIsolation,
BaseStorage,
StateType,
StorageKey,
)
@dataclass

View file

@ -2,7 +2,16 @@ from __future__ import annotations
import abc
import secrets
from typing import TYPE_CHECKING, Any, Dict, Generator, Generic, Optional, TypeVar, Union
from typing import (
TYPE_CHECKING,
Any,
Dict,
Generator,
Generic,
Optional,
TypeVar,
Union,
)
from pydantic import BaseConfig, BaseModel, Extra, root_validator
from pydantic.generics import GenericModel

View file

@ -5,7 +5,9 @@ from .audio import Audio
from .base import UNSET, TelegramObject
from .bot_command import BotCommand
from .bot_command_scope import BotCommandScope
from .bot_command_scope_all_chat_administrators import BotCommandScopeAllChatAdministrators
from .bot_command_scope_all_chat_administrators import (
BotCommandScopeAllChatAdministrators,
)
from .bot_command_scope_all_group_chats import BotCommandScopeAllGroupChats
from .bot_command_scope_all_private_chats import BotCommandScopeAllPrivateChats
from .bot_command_scope_chat import BotCommandScopeChat
@ -110,7 +112,9 @@ from .passport_element_error_front_side import PassportElementErrorFrontSide
from .passport_element_error_reverse_side import PassportElementErrorReverseSide
from .passport_element_error_selfie import PassportElementErrorSelfie
from .passport_element_error_translation_file import PassportElementErrorTranslationFile
from .passport_element_error_translation_files import PassportElementErrorTranslationFiles
from .passport_element_error_translation_files import (
PassportElementErrorTranslationFiles,
)
from .passport_element_error_unspecified import PassportElementErrorUnspecified
from .passport_file import PassportFile
from .photo_size import PhotoSize

View file

@ -5,7 +5,11 @@ from typing import TYPE_CHECKING, Any, List, Optional, Union
from pydantic import Field
from aiogram.utils.text_decorations import TextDecoration, html_decoration, markdown_decoration
from aiogram.utils.text_decorations import (
TextDecoration,
html_decoration,
markdown_decoration,
)
from ..enums import ContentType
from .base import UNSET, TelegramObject
@ -2465,7 +2469,7 @@ class Message(TelegramObject):
**kwargs,
)
def send_copy(
def send_copy( # noqa: C901
self: Message,
chat_id: Union[str, int],
disable_notification: Optional[bool] = None,
@ -2538,7 +2542,7 @@ class Message(TelegramObject):
if self.text:
return SendMessage(text=text, entities=entities, **kwargs)
elif self.audio:
if self.audio:
return SendAudio(
audio=self.audio.file_id,
caption=text,
@ -2548,29 +2552,29 @@ class Message(TelegramObject):
caption_entities=entities,
**kwargs,
)
elif self.animation:
if self.animation:
return SendAnimation(
animation=self.animation.file_id, caption=text, caption_entities=entities, **kwargs
)
elif self.document:
if self.document:
return SendDocument(
document=self.document.file_id, caption=text, caption_entities=entities, **kwargs
)
elif self.photo:
if self.photo:
return SendPhoto(
photo=self.photo[-1].file_id, caption=text, caption_entities=entities, **kwargs
)
elif self.sticker:
if self.sticker:
return SendSticker(sticker=self.sticker.file_id, **kwargs)
elif self.video:
if self.video:
return SendVideo(
video=self.video.file_id, caption=text, caption_entities=entities, **kwargs
)
elif self.video_note:
if self.video_note:
return SendVideoNote(video_note=self.video_note.file_id, **kwargs)
elif self.voice:
if self.voice:
return SendVoice(voice=self.voice.file_id, **kwargs)
elif self.contact:
if self.contact:
return SendContact(
phone_number=self.contact.phone_number,
first_name=self.contact.first_name,
@ -2578,7 +2582,7 @@ class Message(TelegramObject):
vcard=self.contact.vcard,
**kwargs,
)
elif self.venue:
if self.venue:
return SendVenue(
latitude=self.venue.location.latitude,
longitude=self.venue.location.longitude,
@ -2588,19 +2592,19 @@ class Message(TelegramObject):
foursquare_type=self.venue.foursquare_type,
**kwargs,
)
elif self.location:
if self.location:
return SendLocation(
latitude=self.location.latitude, longitude=self.location.longitude, **kwargs
)
elif self.poll:
if self.poll:
return SendPoll(
question=self.poll.question,
options=[option.text for option in self.poll.options],
**kwargs,
)
elif self.dice: # Dice value can't be controlled
if self.dice: # Dice value can't be controlled
return SendDice(**kwargs)
else:
raise TypeError("This type of message can't be copied.")
def copy_to(
@ -3066,9 +3070,10 @@ class Message(TelegramObject):
if self.chat.type in ("private", "group"):
return None
if not self.chat.username or force_private:
chat_value = f"c/{self.chat.shifted_id}"
else:
chat_value = self.chat.username
chat_value = (
f"c/{self.chat.shifted_id}"
if not self.chat.username or force_private
else self.chat.username
)
return f"https://t.me/{chat_value}/{self.message_id}"

View file

@ -15,7 +15,7 @@ def check_signature(token: str, hash: str, **kwargs: Any) -> bool:
:return:
"""
secret = hashlib.sha256(token.encode("utf-8"))
check_string = "\n".join(map(lambda k: f"{k}={kwargs[k]}", sorted(kwargs)))
check_string = "\n".join(f"{k}={kwargs[k]}" for k in sorted(kwargs))
hmac_string = hmac.new(
secret.digest(), check_string.encode("utf-8"), digestmod=hashlib.sha256
).hexdigest()

View file

@ -77,4 +77,7 @@ class Backoff:
self._next_delay = self.min_delay
def __str__(self) -> str:
return f"Backoff(tryings={self._counter}, current_delay={self._current_delay}, next_delay={self._next_delay})"
return (
f"Backoff(tryings={self._counter}, current_delay={self._current_delay}, "
f"next_delay={self._next_delay})"
)

View file

@ -23,7 +23,8 @@ class ChatActionSender:
Provides simply to use context manager.
Technically sender start background task with infinity loop which works
until action will be finished and sends the `chat action <https://core.telegram.org/bots/api#sendchataction>`_
until action will be finished and sends the
`chat action <https://core.telegram.org/bots/api#sendchataction>`_
every 5 seconds.
"""
@ -110,7 +111,7 @@ class ChatActionSender:
async with self._lock:
if not self.running:
return
if not self._close_event.is_set():
if not self._close_event.is_set(): # pragma: no branches
self._close_event.set()
await self._closed_event.wait()
self._task = None

View file

@ -96,7 +96,8 @@ class KeyboardBuilder(Generic[ButtonType]):
"""
if not isinstance(row, list):
raise ValueError(
f"Row {row!r} should be type 'List[{self._button_type.__name__}]' not type {type(row).__name__}"
f"Row {row!r} should be type 'List[{self._button_type.__name__}]' "
f"not type {type(row).__name__}"
)
if len(row) > MAX_WIDTH:
raise ValueError(f"Row {row!r} is too long (MAX_WIDTH={MAX_WIDTH})")
@ -114,7 +115,8 @@ class KeyboardBuilder(Generic[ButtonType]):
count = 0
if not isinstance(markup, list):
raise ValueError(
f"Markup should be type 'List[List[{self._button_type.__name__}]]' not type {type(markup).__name__!r}"
f"Markup should be type 'List[List[{self._button_type.__name__}]]' "
f"not type {type(markup).__name__!r}"
)
for row in markup:
self._validate_row(row)
@ -206,7 +208,8 @@ class KeyboardBuilder(Generic[ButtonType]):
By default, when the sum of passed sizes is lower than buttons count the last
one size will be used for tail of the markup.
If repeat=True is passed - all sizes will be cycled when available more buttons count than all sizes
If repeat=True is passed - all sizes will be cycled when available more buttons
count than all sizes
:param sizes:
:param repeat:

View file

@ -12,7 +12,8 @@ class AsFilterResultOperation(BaseOperation):
self.name = name
def resolve(self, value: Any, initial_value: Any) -> Any:
if value:
if not value:
return None
return {self.name: value}

View file

@ -81,13 +81,12 @@ class TextDecoration(ABC):
:param entities: Array of MessageEntities
:return:
"""
result = "".join(
return "".join(
self._unparse_entities(
add_surrogates(text),
sorted(entities, key=lambda item: item.offset) if entities else [],
)
)
return result
def _unparse_entities(
self,

View file

@ -17,7 +17,10 @@ class WebAppUser(TelegramObject):
"""
id: int
"""A unique identifier for the user or bot. This number may have more than 32 significant bits and some programming languages may have difficulty/silent defects in interpreting it. It has at most 52 significant bits, so a 64-bit integer or a double-precision float type is safe for storing this identifier."""
"""A unique identifier for the user or bot. This number may have more than 32 significant bits
and some programming languages may have difficulty/silent defects in interpreting it.
It has at most 52 significant bits, so a 64-bit integer or a double-precision float type
is safe for storing this identifier."""
is_bot: Optional[bool] = None
"""True, if this user is a bot. Returns in the receiver field only."""
first_name: str
@ -29,24 +32,32 @@ class WebAppUser(TelegramObject):
language_code: Optional[str] = None
"""IETF language tag of the user's language. Returns in user field only."""
photo_url: Optional[str] = None
"""URL of the users profile photo. The photo can be in .jpeg or .svg formats. Only returned for Web Apps launched from the attachment menu."""
"""URL of the users profile photo. The photo can be in .jpeg or .svg formats.
Only returned for Web Apps launched from the attachment menu."""
class WebAppInitData(TelegramObject):
"""
This object contains data that is transferred to the Web App when it is opened. It is empty if the Web App was launched from a keyboard button.
This object contains data that is transferred to the Web App when it is opened.
It is empty if the Web App was launched from a keyboard button.
Source: https://core.telegram.org/bots/webapps#webappinitdata
"""
query_id: Optional[str] = None
"""A unique identifier for the Web App session, required for sending messages via the answerWebAppQuery method."""
"""A unique identifier for the Web App session, required for sending messages
via the answerWebAppQuery method."""
user: Optional[WebAppUser] = None
"""An object containing data about the current user."""
receiver: Optional[WebAppUser] = None
"""An object containing data about the chat partner of the current user in the chat where the bot was launched via the attachment menu. Returned only for Web Apps launched via the attachment menu."""
"""An object containing data about the chat partner of the current user in the chat where
the bot was launched via the attachment menu.
Returned only for Web Apps launched via the attachment menu."""
start_param: Optional[str] = None
"""The value of the startattach parameter, passed via link. Only returned for Web Apps when launched from the attachment menu via link. The value of the start_param parameter will also be passed in the GET-parameter tgWebAppStartParam, so the Web App can load the correct interface right away."""
"""The value of the startattach parameter, passed via link.
Only returned for Web Apps when launched from the attachment menu via link.
The value of the start_param parameter will also be passed in the GET-parameter
tgWebAppStartParam, so the Web App can load the correct interface right away."""
auth_date: datetime
"""Unix time when the form was opened."""
hash: str

View file

@ -88,7 +88,8 @@ class BaseRequestHandler(ABC):
) -> None:
"""
:param dispatcher: instance of :class:`aiogram.dispatcher.dispatcher.Dispatcher`
:param handle_in_background: immediately respond to the Telegram instead of waiting end of handler process
:param handle_in_background: immediately respond to the Telegram instead of
waiting end of handler process
"""
self.dispatcher = dispatcher
self.handle_in_background = handle_in_background
@ -166,7 +167,8 @@ class SimpleRequestHandler(BaseRequestHandler):
) -> None:
"""
:param dispatcher: instance of :class:`aiogram.dispatcher.dispatcher.Dispatcher`
:param handle_in_background: immediately respond to the Telegram instead of waiting end of handler process
:param handle_in_background: immediately respond to the Telegram instead of
waiting end of handler process
:param bot: instance of :class:`aiogram.client.bot.Bot`
"""
super().__init__(dispatcher=dispatcher, handle_in_background=handle_in_background, **data)
@ -184,7 +186,8 @@ class SimpleRequestHandler(BaseRequestHandler):
class TokenBasedRequestHandler(BaseRequestHandler):
"""
Handler that supports multiple bots, the context will be resolved from path variable 'bot_token'
Handler that supports multiple bots, the context will be resolved
from path variable 'bot_token'
"""
def __init__(
@ -196,7 +199,8 @@ class TokenBasedRequestHandler(BaseRequestHandler):
) -> None:
"""
:param dispatcher: instance of :class:`aiogram.dispatcher.dispatcher.Dispatcher`
:param handle_in_background: immediately respond to the Telegram instead of waiting end of handler process
:param handle_in_background: immediately respond to the Telegram instead of
waiting end of handler process
:param bot_settings: kwargs that will be passed to new Bot instance
"""
super().__init__(dispatcher=dispatcher, handle_in_background=handle_in_background, **data)

213
docs/contributing.rst Normal file
View file

@ -0,0 +1,213 @@
============
Contributing
============
You're welcome to contribute to aiogram!
*aiogram* is an open-source project, and anyone can contribute to it in any possible way
Developing
==========
Before making any changes in the framework code, it is necessary to fork the project and clone
the project to your PC and know how to do a pull-request.
How to work with pull-request you can read in the `GitHub docs <https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request>`_
Also in due to this project is written in Python, you will need Python to be installed
(is recommended to use latest Python versions, but any version starting from 3.8 can be used)
Use virtualenv
--------------
You can create a virtual environment in a directory using :code:`venv` module (it should be pre-installed by default):
.. code-block::bash
python -m venv .venv
This action will create a :code:`.venv` directory with the Python binaries and then you will
be able to install packages into that isolated environment.
Activate the environment
------------------------
Linux/ macOS:
.. code-block:: bash
source .venv/bin/activate
Windows PoweShell
.. code-block:: powershell
.\.venv\Scripts\Activate.ps1
To check it worked, use described command, it should show the :code:`pip` location inside
the isolated environment
Linux, macOS:
.. code-block::
which pip
Windows PowerShell
.. code-block::
Get-Command pip
Also make you shure you have the latest pip version in your virtual environment to avoid
errors on next steps:
.. code-block::
python -m pip install --upgrade pip
Setup project
-------------
After activating the environment install `aiogram` from sources and their dependencies:
.. code-block:: bash
pip install -e ."[dev,test,docs,fast,redis,proxy,i18n]"
It will install :code:`aiogram` in editable mode into your virtual environment and all dependencies.
Making changes in code
----------------------
At this point you can make any changes in the code that you want, it can be any fixes,
implementing new features or experimenting.
Format the code (code-style)
----------------------------
Note that this project is Black-formatted, so you should follow that code-style,
too be sure You're correctly doing this let's reformat the code automatically:
.. code-block:: bash
black aiogram tests examples
isort aiogram tests examples
Run tests
---------
All changes should be tested:
.. code-block:: bash
pytest tests
Also if you are doing something with Redis-storage, you will need to test everything works with Redis:
.. code-block:: bash
pytest --redis redis://<host>:<port>/<db> tests
Docs
----
We are using `Sphinx` to render docs in different languages, all sources located in `docs` directory,
you can change the sources and to test it you can start live-preview server and look what you are doing:
.. code-block:: bash
sphinx-autobuild --watch aiogram/ docs/ docs/_build/
Docs translations
-----------------
Translation of the documentation is very necessary and cannot be done without the help of the
community from all over the world, so you are welcome to translate the documentation
into different languages.
Before start, let's up to date all texts:
.. code-block:: bash
cd docs
make gettext
sphinx-intl update -p _build/gettext -l <language_code>
Change the :code:`<language_code>` in example below to the target language code, after that
you can modify texts inside :code:`docs/locale/<language_code>/LC_MESSAGES` as :code:`*.po` files
by using any text-editor or specialized utilites for GNU Gettext,
for example via `poedit <https://poedit.net/>`_.
To view results:
.. code-block:: bash
sphinx-autobuild --watch aiogram/ docs/ docs/_build/ -D language=<language_code>
Describe changes
----------------
Describe your changes in one or more sentences so that bot developers know what's changed
in their favorite framework - create `<code>.<category>.rst` file and write the description.
:code:`<code>` is Issue or Pull-request number, after release link to this issue will
be published to the *Changelog* page.
:code:`<category>` is a changes category marker, it can be one of:
- :code:`feature` - when you are implementing new feature
- :code:`bugfix` - when you fix a bug
- :code:`doc` - when you improve the docs
- :code:`removal` - when you remove something from the framework
- :code:`misc` - when changed something inside the Core or project configuration
If you have troubles with changing category feel free to ask Core-contributors to help with choosing it.
Complete
--------
After you have made all your changes, publish them to the repository and create a pull request
as mentioned at the beginning of the article and wait for a review of these changes.
Star on GitHub
==============
You can "star" repository on GitHub - https://github.com/aiogram/aiogram (click the star button at the top right)
Adding stars makes it easier for other people to find this project and understand how useful it is.
Guides
======
You can write guides how to develop Bots on top of aiogram and publish it into YouTube, Medium,
GitHub Books, any Courses platform or any other platform that you know.
This will help more people learn about the framework and learn how to use it
Take answers
============
The developers is always asks for any question in our chats or any other platforms like GitHub Discussions,
StackOverflow and others, feel free to answer to this questions.
Funding
=======
The development of the project is free and not financed by commercial organizations,
it is my personal initiative (`@JRootJunior <https://t.me/JRootJunior>`_) and
I am engaged in the development of the project in my free time.
So, if you want to financially support the project, or, for example, give me a pizza or a beer,
you can do it on `OpenCollective <https://opencollective.com/aiogram>`_
or `Patreon <https://www.patreon.com/aiogram>`_.

View file

@ -16,3 +16,4 @@ Contents
dispatcher/index
utils/index
changelog
contributing

View file

@ -8,7 +8,12 @@ from aiogram import Bot, Dispatcher, F, Router, html
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import KeyboardButton, Message, ReplyKeyboardMarkup, ReplyKeyboardRemove
from aiogram.types import (
KeyboardButton,
Message,
ReplyKeyboardMarkup,
ReplyKeyboardRemove,
)
form_router = Router()

View file

@ -48,7 +48,7 @@ async def command_add_bot(message: Message, command: CommandObject, bot: Bot) ->
return message.answer("Invalid token")
await new_bot.delete_webhook(drop_pending_updates=True)
await new_bot.set_webhook(OTHER_BOTS_URL.format(bot_token=command.args))
await message.answer(f"Bot @{bot_user.username} successful added")
return await message.answer(f"Bot @{bot_user.username} successful added")
async def on_startup(dispatcher: Dispatcher, bot: Bot):

View file

@ -36,7 +36,6 @@ async def send_message_handler(request: Request):
except ValueError:
return json_response({"ok": False, "err": "Unauthorized"}, status=401)
print(data)
reply_markup = None
if data["with_webview"] == "1":
reply_markup = InlineKeyboardMarkup(

View file

@ -1,37 +0,0 @@
[mypy]
plugins = pydantic.mypy
python_version = 3.8
show_error_codes = True
show_error_context = True
pretty = True
ignore_missing_imports = False
warn_unused_configs = True
disallow_subclassing_any = True
disallow_any_generics = True
disallow_untyped_calls = True
disallow_untyped_defs = True
disallow_incomplete_defs = True
check_untyped_defs = True
disallow_untyped_decorators = True
no_implicit_optional = True
warn_redundant_casts = True
warn_unused_ignores = True
warn_return_any = True
follow_imports_for_stubs = True
namespace_packages = True
show_absolute_path = True
[mypy-aiofiles]
ignore_missing_imports = True
[mypy-async_lru]
ignore_missing_imports = True
[mypy-uvloop]
ignore_missing_imports = True
[mypy-redis.*]
ignore_missing_imports = True
[mypy-babel.*]
ignore_missing_imports = True

2486
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,18 +1,19 @@
[tool.poetry]
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "aiogram"
version = "3.0.0-beta.7"
description = "Modern and fully asynchronous framework for Telegram Bot API"
description = 'Modern and fully asynchronous framework for Telegram Bot API'
readme = "README.rst"
requires-python = ">=3.8"
license = "MIT"
authors = [
"Alex Root Junior <jroot.junior@gmail.com>",
{ name = "Alex Root Junior", email = "jroot.junior@gmail.com" },
]
maintainers = [
"Alex Root Junior <jroot.junior@gmail.com>",
{ name = "Alex Root Junior", email = "jroot.junior@gmail.com" },
]
license = "MIT"
readme = "README.rst"
homepage = "https://aiogram.dev/"
documentation = "https://docs.aiogram.dev/"
repository = "https://github.com/aiogram/aiogram/"
keywords = [
"telegram",
"bot",
@ -38,77 +39,245 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Communications :: Chat",
]
packages = [
{ include = "aiogram" }
dependencies = [
"magic-filter~=1.0.9",
"aiohttp~=3.8.3",
"pydantic~=1.10.4",
"aiofiles~=22.1.0",
"certifi>=2022.9.24",
]
dynamic = ["version"]
[tool.hatch.version]
path = "aiogram/__init__.py"
[project.optional-dependencies]
fast = [
"uvloop>=0.17.0; (sys_platform == 'darwin' or sys_platform == 'linux') and platform_python_implementation != 'PyPy'",
]
redis = [
"redis~=4.3.4",
]
proxy = [
"aiohttp-socks~=0.7.1",
]
i18n = [
"Babel~=2.9.1",
]
test = [
"pytest~=7.1.3",
"pytest-html~=3.1.1",
"pytest-asyncio~=0.19.0",
"pytest-lazy-fixture~=0.6.3",
"pytest-mock~=3.9.0",
"pytest-mypy~=0.10.0",
"pytest-cov~=4.0.0",
"pytest-aiohttp~=1.0.4",
"aresponses~=2.1.6",
]
docs = [
"Sphinx~=5.2.3",
"sphinx-intl~=2.0.1",
"sphinx-autobuild~=2021.3.14",
"sphinx-copybutton~=0.5.0",
"furo~=2022.9.29",
"sphinx-prompt~=1.5.0",
"Sphinx-Substitution-Extensions~=2022.2.16",
"towncrier~=22.8.0",
"pygments~=2.4",
"pymdown-extensions~=9.6",
"markdown-include~=0.7.0",
"Pygments~=2.13.0",
"sphinxcontrib-towncrier~=0.3.1a3",
]
dev = [
"black~=22.8",
"isort~=5.11",
"ruff~=0.0.215",
"mypy~=0.981",
"toml~=0.10.2",
"pre-commit~=2.20.0",
"packaging~=21.3",
"typing-extensions~=4.3.0",
]
[project.urls]
Homepage = "https://aiogram.dev/"
Documentation = "https://docs.aiogram.dev/"
Repository = "https://github.com/aiogram/aiogram/"
[tool.poetry.dependencies]
python = "^3.8"
magic-filter = "^1.0.9"
aiohttp = "^3.8.3"
pydantic = "^1.10.2"
aiofiles = "^22.1.0"
# Fast
uvloop = { version = "^0.17.0", markers = "sys_platform == 'darwin' or sys_platform == 'linux'", optional = true }
# i18n
Babel = { version = "^2.9.1", optional = true }
# Proxy
aiohttp-socks = { version = "^0.7.1", optional = true }
# Redis
redis = { version = "^4.3.4", optional = true }
certifi = "^2022.9.24"
[tool.hatch.envs.default]
features = [
"dev",
"fast",
"redis",
"proxy",
"i18n",
]
post-install-commands = [
"pre-commit install",
]
[tool.hatch.envs.default.scripts]
reformat = [
"black aiogram tests",
"isort aiogram tests",
]
lint = "ruff aiogram"
[tool.hatch.envs.docs]
features = [
"fast",
"redis",
"proxy",
"i18n",
"docs",
]
[tool.hatch.envs.docs.scripts]
serve = "sphinx-autobuild --watch aiogram/ --watch CHANGELOG.rst --watch README.rst docs/ docs/_build/ {args}"
[tool.hatch.envs.dev]
python = "3.10"
features = [
"dev",
"fast",
"redis",
"proxy",
"i18n",
"test",
]
extra-dependencies = [
"butcher @ git+https://github.com/aiogram/butcher.git@v0.1.9"
]
[tool.hatch.envs.test]
features = [
"fast",
"redis",
"proxy",
"i18n",
"test",
]
[tool.hatch.envs.test.scripts]
cov = [
"pytest --cov-config pyproject.toml --cov=aiogram --html=reports/py{matrix:python}/tests/index.html {args}",
"coverage html -d reports/py{matrix:python}/coverage",
]
cov-redis = [
"pytest --cov-config pyproject.toml --cov=aiogram --html=reports/py{matrix:python}/tests/index.html --redis {env:REDIS_DNS:'redis://localhost:6379'} {args}",
"coverage html -d reports/py{matrix:python}/coverage",
]
view-cov = "google-chrome-stable reports/py{matrix:python}/coverage/index.html"
[tool.poetry.group.docs.dependencies]
Sphinx = "^5.2.3"
sphinx-intl = "^2.0.1"
sphinx-autobuild = "^2021.3.14"
sphinx-copybutton = "^0.5.0"
furo = "^2022.9.29"
sphinx-prompt = "^1.5.0"
Sphinx-Substitution-Extensions = "^2022.2.16"
towncrier = "^22.8.0"
pygments = "^2.4"
pymdown-extensions = "^9.6"
markdown-include = "^0.7.0"
Pygments = "^2.13.0"
sphinxcontrib-towncrier = "^0.3.1a3"
[[tool.hatch.envs.test.matrix]]
python = ["38", "39", "310", "311"]
[tool.ruff]
line-length = 99
select = [
# "C", # TODO: mccabe - code complecity
"C4",
"E",
"F",
"T10",
"T20",
"Q",
"RET",
]
ignore = [
"F401"
]
src = ["aiogram", "tests"]
exclude = [
".git",
"build",
"dist",
"venv",
".venv",
"docs",
"tests",
"dev",
"scripts",
"*.egg-info",
]
target-version = "py310"
[tool.ruff.isort]
known-first-party = [
"aiogram",
"finite_state_machine",
"handlers",
"routes",
]
[tool.ruff.per-file-ignores]
"aiogram/client/bot.py" = ["E501"]
"aiogram/types/*" = ["E501"]
"aiogram/methods/*" = ["E501"]
"aiogram/enums/*" = ["E501"]
[tool.poetry.group.test.dependencies]
pytest = "^7.1.3"
pytest-html = "^3.1.1"
pytest-asyncio = "^0.19.0"
pytest-lazy-fixture = "^0.6.3"
pytest-mock = "^3.9.0"
pytest-mypy = "^0.10.0"
pytest-cov = "^4.0.0"
pytest-aiohttp = "^1.0.4"
aresponses = "^2.1.6"
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = [
"tests",
]
[tool.coverage.run]
branch = false
parallel = true
omit = [
"aiogram/__about__.py",
]
[tool.poetry.group.dev.dependencies]
black = "^22.8.0"
isort = "^5.10.1"
flake8 = "^5.0.4"
mypy = "^0.981"
toml = "^0.10.2"
pre-commit = "^2.20.0"
packaging = "^21.3"
typing-extensions = "^4.3.0"
butcher = { git = "https://github.com/aiogram/butcher.git", rev = "v0.1.8", python = "3.10" }
[tool.coverage.report]
exclude_lines = [
"if __name__ == .__main__.:",
"pragma: no cover",
"if TYPE_CHECKING:",
"@abstractmethod",
"@overload",
]
[tool.mypy]
plugins = "pydantic.mypy"
python_version = "3.8"
show_error_codes = true
show_error_context = true
pretty = true
ignore_missing_imports = false
warn_unused_configs = true
disallow_subclassing_any = true
disallow_any_generics = true
disallow_untyped_calls = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_return_any = true
follow_imports_for_stubs = true
namespace_packages = true
show_absolute_path = true
[tool.poetry.extras]
fast = ["uvloop"]
redis = ["redis"]
proxy = ["aiohttp-socks"]
i18n = ["Babel"]
[[tool.mypy.overrides]]
module = [
"aiofiles",
"async_lru",
"uvloop",
"redis.*",
"babel.*",
]
ignore_missing_imports = true
disallow_untyped_defs = true
[tool.black]
line-length = 99
target-version = ['py38', 'py39', 'py310']
target-version = ['py38', 'py39', 'py310', 'py311']
exclude = '''
(
\.eggs
@ -122,21 +291,7 @@ exclude = '''
'''
[tool.isort]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
line_length = 99
known_third_party = [
"aiofiles",
"aiohttp",
"aiohttp_socks",
"aresponses",
"packaging",
"pkg_resources",
"pydantic",
"pytest"
]
profile = "black"
[tool.towncrier]
package = "aiogram"
@ -172,7 +327,3 @@ showcontent = true
directory = "misc"
name = "Misc"
showcontent = true
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View file

@ -1,4 +0,0 @@
[pytest]
asyncio_mode = auto
testpaths =
tests

View file

@ -1,16 +0,0 @@
aiofiles==22.1.0 ; python_version >= "3.8" and python_version < "4.0"
aiohttp==3.8.3 ; python_version >= "3.8" and python_version < "4.0"
aiosignal==1.2.0 ; python_version >= "3.8" and python_version < "4.0"
async-timeout==4.0.2 ; python_version >= "3.8" and python_version < "4.0"
attrs==22.1.0 ; python_version >= "3.8" and python_version < "4.0"
babel==2.10.3 ; python_version >= "3.8" and python_version < "4.0"
certifi==2022.9.24 ; python_version >= "3.8" and python_version < "4.0"
charset-normalizer==2.1.1 ; python_version >= "3.8" and python_version < "4.0"
frozenlist==1.3.1 ; python_version >= "3.8" and python_version < "4.0"
idna==3.4 ; python_version >= "3.8" and python_version < "4.0"
magic-filter==1.0.9 ; python_version >= "3.8" and python_version < "4.0"
multidict==6.0.2 ; python_version >= "3.8" and python_version < "4.0"
pydantic==1.10.2 ; python_version >= "3.8" and python_version < "4.0"
pytz==2022.5 ; python_version >= "3.8" and python_version < "4.0"
typing-extensions==4.4.0 ; python_version >= "3.8" and python_version < "4.0"
yarl==1.8.1 ; python_version >= "3.8" and python_version < "4.0"

View file

@ -1,65 +0,0 @@
aiofiles==22.1.0 ; python_version >= "3.8" and python_version < "4.0"
aiohttp-socks==0.7.1 ; python_version >= "3.8" and python_version < "4.0"
aiohttp==3.8.3 ; python_version >= "3.8" and python_version < "4.0"
aiosignal==1.2.0 ; python_version >= "3.8" and python_version < "4.0"
alabaster==0.7.12 ; python_version >= "3.8" and python_version < "4.0"
async-timeout==4.0.2 ; python_version >= "3.8" and python_version < "4.0"
attrs==22.1.0 ; python_version >= "3.8" and python_version < "4.0"
babel==2.10.3 ; python_version >= "3.8" and python_version < "4.0"
beautifulsoup4==4.11.1 ; python_version >= "3.8" and python_version < "4.0"
certifi==2022.9.24 ; python_version >= "3.8" and python_version < "4.0"
charset-normalizer==2.1.1 ; python_version >= "3.8" and python_version < "4.0"
click-default-group==1.2.2 ; python_version >= "3.8" and python_version < "4.0"
click==8.1.3 ; python_version >= "3.8" and python_version < "4.0"
colorama==0.4.6 ; python_version >= "3.8" and python_version < "4.0"
deprecated==1.2.13 ; python_version >= "3.8" and python_version < "4.0"
docutils==0.19 ; python_version >= "3.8" and python_version < "4.0"
frozenlist==1.3.1 ; python_version >= "3.8" and python_version < "4.0"
furo==2022.9.29 ; python_version >= "3.8" and python_version < "4.0"
idna==3.4 ; python_version >= "3.8" and python_version < "4.0"
imagesize==1.4.1 ; python_version >= "3.8" and python_version < "4.0"
importlib-metadata==5.0.0 ; python_version >= "3.8" and python_version < "3.10"
incremental==22.10.0 ; python_version >= "3.8" and python_version < "4.0"
jinja2==3.1.2 ; python_version >= "3.8" and python_version < "4.0"
livereload==2.6.3 ; python_version >= "3.8" and python_version < "4.0"
magic-filter==1.0.9 ; python_version >= "3.8" and python_version < "4.0"
markdown-include==0.7.0 ; python_version >= "3.8" and python_version < "4.0"
markdown==3.4.1 ; python_version >= "3.8" and python_version < "4.0"
markupsafe==2.1.1 ; python_version >= "3.8" and python_version < "4.0"
multidict==6.0.2 ; python_version >= "3.8" and python_version < "4.0"
packaging==21.3 ; python_version >= "3.8" and python_version < "4.0"
pydantic==1.10.2 ; python_version >= "3.8" and python_version < "4.0"
pygments==2.13.0 ; python_version >= "3.8" and python_version < "4.0"
pymdown-extensions==9.7 ; python_version >= "3.8" and python_version < "4.0"
pyparsing==3.0.9 ; python_version >= "3.8" and python_version < "4.0"
python-socks[asyncio]==2.0.3 ; python_version >= "3.8" and python_version < "4.0"
pytz==2022.5 ; python_version >= "3.8" and python_version < "4.0"
redis==4.3.4 ; python_version >= "3.8" and python_version < "4.0"
requests==2.28.1 ; python_version >= "3.8" and python_version < "4"
setuptools==65.5.0 ; python_version >= "3.8" and python_version < "4.0"
six==1.16.0 ; python_version >= "3.8" and python_version < "4.0"
snowballstemmer==2.2.0 ; python_version >= "3.8" and python_version < "4.0"
soupsieve==2.3.2.post1 ; python_version >= "3.8" and python_version < "4.0"
sphinx-autobuild==2021.3.14 ; python_version >= "3.8" and python_version < "4.0"
sphinx-basic-ng==1.0.0b1 ; python_version >= "3.8" and python_version < "4.0"
sphinx-copybutton==0.5.0 ; python_version >= "3.8" and python_version < "4.0"
sphinx-intl==2.0.1 ; python_version >= "3.8" and python_version < "4.0"
sphinx-prompt==1.5.0 ; python_version >= "3.8" and python_version < "4.0"
sphinx-substitution-extensions==2022.2.16 ; python_version >= "3.8" and python_version < "4.0"
sphinx==5.3.0 ; python_version >= "3.8" and python_version < "4.0"
sphinxcontrib-applehelp==1.0.2 ; python_version >= "3.8" and python_version < "4.0"
sphinxcontrib-devhelp==1.0.2 ; python_version >= "3.8" and python_version < "4.0"
sphinxcontrib-htmlhelp==2.0.0 ; python_version >= "3.8" and python_version < "4.0"
sphinxcontrib-jsmath==1.0.1 ; python_version >= "3.8" and python_version < "4.0"
sphinxcontrib-qthelp==1.0.3 ; python_version >= "3.8" and python_version < "4.0"
sphinxcontrib-serializinghtml==1.1.5 ; python_version >= "3.8" and python_version < "4.0"
sphinxcontrib-towncrier==0.3.1a3 ; python_version >= "3.8" and python_version < "4.0"
tomli==2.0.1 ; python_version >= "3.8" and python_version < "4.0"
tornado==6.2 ; python_version >= "3.8" and python_version < "4.0"
towncrier==22.8.0 ; python_version >= "3.8" and python_version < "4.0"
typing-extensions==4.4.0 ; python_version >= "3.8" and python_version < "4.0"
urllib3==1.26.12 ; python_version >= "3.8" and python_version < "4"
uvloop==0.17.0 ; python_version >= "3.8" and python_version < "4.0" and sys_platform == "darwin" or python_version >= "3.8" and python_version < "4.0" and sys_platform == "linux"
wrapt==1.14.1 ; python_version >= "3.8" and python_version < "4.0"
yarl==1.8.1 ; python_version >= "3.8" and python_version < "4.0"
zipp==3.10.0 ; python_version >= "3.8" and python_version < "3.10"

View file

@ -5,7 +5,11 @@ from _pytest.config import UsageError
from redis.asyncio.connection import parse_url as parse_redis_url
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import DisabledEventIsolation, MemoryStorage, SimpleEventIsolation
from aiogram.fsm.storage.memory import (
DisabledEventIsolation,
MemoryStorage,
SimpleEventIsolation,
)
from aiogram.fsm.storage.redis import RedisEventIsolation, RedisStorage
from tests.mocked_bot import MockedBot

View file

@ -1,6 +1,10 @@
from aiogram import Bot
from aiogram.methods import AnswerInlineQuery, Request
from aiogram.types import InlineQueryResult, InlineQueryResultPhoto, InputTextMessageContent
from aiogram.types import (
InlineQueryResult,
InlineQueryResultPhoto,
InputTextMessageContent,
)
from tests.mocked_bot import MockedBot

View file

@ -1,5 +1,11 @@
from aiogram.methods import AnswerShippingQuery
from aiogram.types import LabeledPrice, ShippingAddress, ShippingOption, ShippingQuery, User
from aiogram.types import (
LabeledPrice,
ShippingAddress,
ShippingOption,
ShippingQuery,
User,
)
class TestInlineQuery:

View file

@ -5,7 +5,15 @@ from typing import Sequence, Type
import pytest
from aiogram.filters import Text
from aiogram.types import CallbackQuery, Chat, InlineQuery, Message, Poll, PollOption, User
from aiogram.types import (
CallbackQuery,
Chat,
InlineQuery,
Message,
Poll,
PollOption,
User,
)
class TestText:

View file

@ -1,7 +1,11 @@
import pytest
from aiogram.fsm.storage.base import DEFAULT_DESTINY, StorageKey
from aiogram.fsm.storage.redis import DefaultKeyBuilder, RedisEventIsolation, RedisStorage
from aiogram.fsm.storage.redis import (
DefaultKeyBuilder,
RedisEventIsolation,
RedisStorage,
)
PREFIX = "test"
BOT_ID = 42

View file

@ -7,7 +7,12 @@ from aiogram.fsm.context import FSMContext
from aiogram.fsm.storage.base import StorageKey
from aiogram.fsm.storage.memory import MemoryStorage
from aiogram.types import Update, User
from aiogram.utils.i18n import ConstI18nMiddleware, FSMI18nMiddleware, I18n, SimpleI18nMiddleware
from aiogram.utils.i18n import (
ConstI18nMiddleware,
FSMI18nMiddleware,
I18n,
SimpleI18nMiddleware,
)
from aiogram.utils.i18n.context import get_i18n, gettext, lazy_gettext
from tests.conftest import DATA_DIR
from tests.mocked_bot import MockedBot
@ -51,24 +56,24 @@ class TestI18nCore:
@pytest.mark.parametrize(
"locale,case,result",
[
[None, dict(singular="test"), "test"],
[None, dict(singular="test", locale="uk"), "тест"],
["en", dict(singular="test", locale="uk"), "тест"],
["uk", dict(singular="test", locale="uk"), "тест"],
["uk", dict(singular="test"), "тест"],
["it", dict(singular="test"), "test"],
[None, dict(singular="test", n=2), "test"],
[None, dict(singular="test", n=2, locale="uk"), "тест"],
["en", dict(singular="test", n=2, locale="uk"), "тест"],
["uk", dict(singular="test", n=2, locale="uk"), "тест"],
["uk", dict(singular="test", n=2), "тест"],
["it", dict(singular="test", n=2), "test"],
[None, dict(singular="test", plural="test2", n=2), "test2"],
[None, dict(singular="test", plural="test2", n=2, locale="uk"), "test2"],
["en", dict(singular="test", plural="test2", n=2, locale="uk"), "test2"],
["uk", dict(singular="test", plural="test2", n=2, locale="uk"), "test2"],
["uk", dict(singular="test", plural="test2", n=2), "test2"],
["it", dict(singular="test", plural="test2", n=2), "test2"],
[None, {"singular": "test"}, "test"],
[None, {"singular": "test", "locale": "uk"}, "тест"],
["en", {"singular": "test", "locale": "uk"}, "тест"],
["uk", {"singular": "test", "locale": "uk"}, "тест"],
["uk", {"singular": "test"}, "тест"],
["it", {"singular": "test"}, "test"],
[None, {"singular": "test", "n": 2}, "test"],
[None, {"singular": "test", "n": 2, "locale": "uk"}, "тест"],
["en", {"singular": "test", "n": 2, "locale": "uk"}, "тест"],
["uk", {"singular": "test", "n": 2, "locale": "uk"}, "тест"],
["uk", {"singular": "test", "n": 2}, "тест"],
["it", {"singular": "test", "n": 2}, "test"],
[None, {"singular": "test", "plural": "test2", "n": 2}, "test2"],
[None, {"singular": "test", "plural": "test2", "n": 2, "locale": "uk"}, "test2"],
["en", {"singular": "test", "plural": "test2", "n": 2, "locale": "uk"}, "test2"],
["uk", {"singular": "test", "plural": "test2", "n": 2, "locale": "uk"}, "test2"],
["uk", {"singular": "test", "plural": "test2", "n": 2}, "test2"],
["it", {"singular": "test", "plural": "test2", "n": 2}, "test2"],
],
)
def test_gettext(self, i18n: I18n, locale: str, case: Dict[str, Any], result: str):
@ -110,8 +115,17 @@ class TestSimpleI18nMiddleware:
middleware = SimpleI18nMiddleware(i18n=i18n)
middleware.setup(router=dp)
assert middleware not in dp.update.outer_middleware
assert middleware in dp.message.outer_middleware
assert middleware in dp.callback_query.outer_middleware
async def test_setup_exclude(self, i18n: I18n):
dp = Dispatcher()
middleware = SimpleI18nMiddleware(i18n=i18n)
middleware.setup(router=dp, exclude={"message"})
assert middleware not in dp.update.outer_middleware
assert middleware not in dp.message.outer_middleware
assert middleware in dp.callback_query.outer_middleware
async def test_get_unknown_locale(self, i18n: I18n):
dp = Dispatcher()
@ -131,6 +145,19 @@ class TestSimpleI18nMiddleware:
)
assert locale == i18n.default_locale
async def test_custom_keys(self, i18n: I18n):
async def handler(event, data):
return data
middleware = SimpleI18nMiddleware(
i18n=i18n, i18n_key="translator", middleware_key="middleware"
)
context: dict[str, Any] = await middleware(handler, None, {})
assert "translator" in context
assert context["translator"] == i18n
assert "middleware" in context
assert context["middleware"] == middleware
class TestConstI18nMiddleware:
async def test_middleware(self, i18n: I18n):
@ -138,13 +165,17 @@ class TestConstI18nMiddleware:
result = await middleware(
next_call,
Update(update_id=42),
{"event_from_user": User(id=42, is_bot=False, language_code="it", first_name="Test")},
{
"event_from_user": User(
id=42, is_bot=False, language_code="it", first_name="Test"
),
},
)
assert result == "тест"
class TestFSMI18nMiddleware:
async def test_middleware(self, i18n: I18n, bot: MockedBot):
async def test_middleware(self, i18n: I18n, bot: MockedBot, extra):
middleware = FSMI18nMiddleware(i18n=i18n)
storage = MemoryStorage()
state = FSMContext(
@ -160,3 +191,16 @@ class TestFSMI18nMiddleware:
assert i18n.current_locale == "uk"
result = await middleware(next_call, Update(update_id=42), data)
assert result == "тест"
async def test_without_state(self, i18n: I18n, bot: MockedBot, extra):
middleware = FSMI18nMiddleware(i18n=i18n)
data = {
"event_from_user": User(id=42, is_bot=False, language_code="it", first_name="Test"),
}
result = await middleware(next_call, Update(update_id=42), data)
assert i18n.current_locale == "en"
assert result == "test"
assert i18n.current_locale == "en"
result = await middleware(next_call, Update(update_id=42), data)
assert i18n.current_locale == "en"

View file

@ -7,7 +7,11 @@ from aiogram.types import (
KeyboardButton,
ReplyKeyboardMarkup,
)
from aiogram.utils.keyboard import InlineKeyboardBuilder, KeyboardBuilder, ReplyKeyboardBuilder
from aiogram.utils.keyboard import (
InlineKeyboardBuilder,
KeyboardBuilder,
ReplyKeyboardBuilder,
)
class MyCallback(CallbackData, prefix="test"):

View file

@ -3,7 +3,11 @@ from typing import List, Optional
import pytest
from aiogram.types import MessageEntity, User
from aiogram.utils.text_decorations import TextDecoration, html_decoration, markdown_decoration
from aiogram.utils.text_decorations import (
TextDecoration,
html_decoration,
markdown_decoration,
)
class TestTextDecoration: