Merge pull request #283 from b0g3r/dev-3.x-strict-mypy

🏷️ Turn on mypy's strict mode
This commit is contained in:
Alex Root Junior 2020-03-29 15:08:57 +03:00 committed by GitHub
commit 4cb9697cb4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 539 additions and 323 deletions

View file

@ -38,7 +38,7 @@ jobs:
- name: Lint code - name: Lint code
run: | run: |
poetry run flake8 aiogram test poetry run flake8 aiogram test
poetry run mypy aiogram tests poetry run mypy aiogram
- name: Run tests - name: Run tests
run: | run: |

View file

@ -86,11 +86,11 @@ flake8-report:
.PHONY: mypy .PHONY: mypy
mypy: mypy:
$(py) mypy aiogram tests $(py) mypy aiogram
.PHONY: mypy-report .PHONY: mypy-report
mypy-report: mypy-report:
$(py) mypy aiogram tests --html-report $(reports_dir)/typechecking $(py) mypy aiogram --html-report $(reports_dir)/typechecking
.PHONY: lint .PHONY: lint
lint: isort black flake8 mypy lint: isort black flake8 mypy

View file

@ -1,89 +0,0 @@
from __future__ import annotations
from contextlib import asynccontextmanager
from typing import Any, Optional, TypeVar
from ...utils.mixins import ContextInstanceMixin, DataMixin
from ...utils.token import extract_bot_id, validate_token
from ..methods import TelegramMethod
from .session.aiohttp import AiohttpSession
from .session.base import BaseSession
T = TypeVar("T")
class BaseBot(ContextInstanceMixin, DataMixin):
"""
Base class for bots
"""
def __init__(
self, token: str, session: BaseSession = None, parse_mode: Optional[str] = None
) -> None:
validate_token(token)
if session is None:
session = AiohttpSession()
self.session = session
self.parse_mode = parse_mode
self.__token = token
@property
def id(self) -> int:
"""
Get bot ID from token
:return:
"""
return extract_bot_id(self.__token)
async def __call__(self, method: TelegramMethod[T]) -> T:
"""
Call API method
:param method:
:return:
"""
return await self.session.make_request(self.__token, method)
async def close(self) -> None:
"""
Close bot session
"""
await self.session.close()
@asynccontextmanager
async def context(self, auto_close: bool = True):
"""
Generate bot context
:param auto_close:
:return:
"""
token = self.set_current(self)
try:
yield self
finally:
if auto_close:
await self.close()
self.reset_current(token)
def __hash__(self) -> int:
"""
Get hash for the token
:return:
"""
return hash(self.__token)
def __eq__(self, other: Any) -> bool:
"""
Compare current bot with another bot instance
:param other:
:return:
"""
if not isinstance(other, BaseBot):
return False
return hash(self) == hash(other)

View file

@ -1,8 +1,20 @@
from __future__ import annotations
import datetime import datetime
from typing import List, Optional, Union from contextlib import asynccontextmanager
from typing import (
List,
Optional,
Union,
TypeVar,
AsyncIterator,
Any,
)
from async_lru import alru_cache from async_lru import alru_cache
from .session.aiohttp import AiohttpSession
from .session.base import BaseSession
from ..methods import ( from ..methods import (
AddStickerToSet, AddStickerToSet,
AnswerCallbackQuery, AnswerCallbackQuery,
@ -70,6 +82,7 @@ from ..methods import (
UnbanChatMember, UnbanChatMember,
UnpinChatMessage, UnpinChatMessage,
UploadStickerFile, UploadStickerFile,
TelegramMethod,
) )
from ..types import ( from ..types import (
Chat, Chat,
@ -98,15 +111,92 @@ from ..types import (
UserProfilePhotos, UserProfilePhotos,
WebhookInfo, WebhookInfo,
) )
from .base import BaseBot from ...utils.mixins import ContextInstanceMixin
from ...utils.token import (
validate_token,
extract_bot_id,
)
T = TypeVar("T")
class Bot(BaseBot): class Bot(ContextInstanceMixin["Bot"]):
""" """
Class where located all API methods Class where located all API methods
""" """
@alru_cache() def __init__(
self, token: str, session: Optional[BaseSession] = None, parse_mode: Optional[str] = None
) -> None:
validate_token(token)
if session is None:
session = AiohttpSession()
self.session = session
self.parse_mode = parse_mode
self.__token = token
@property
def id(self) -> int:
"""
Get bot ID from token
:return:
"""
return extract_bot_id(self.__token)
async def __call__(self, method: TelegramMethod[T]) -> T:
"""
Call API method
:param method:
:return:
"""
return await self.session.make_request(self.__token, method)
async def close(self) -> None:
"""
Close bot session
"""
await self.session.close()
@asynccontextmanager
async def context(self, auto_close: bool = True) -> AsyncIterator[Bot]:
"""
Generate bot context
:param auto_close:
:return:
"""
token = self.set_current(self)
try:
yield self
finally:
if auto_close:
await self.close()
self.reset_current(token)
def __hash__(self) -> int:
"""
Get hash for the token
:return:
"""
return hash(self.__token)
def __eq__(self, other: Any) -> bool:
"""
Compare current bot with another bot instance
:param other:
:return:
"""
if not isinstance(other, Bot):
return False
return hash(self) == hash(other)
@alru_cache() # type: ignore
async def me(self) -> User: async def me(self) -> User:
return await self.get_me() return await self.get_me()

View file

@ -15,8 +15,8 @@ class AiohttpSession(BaseSession):
def __init__( def __init__(
self, self,
api: TelegramAPIServer = PRODUCTION, api: TelegramAPIServer = PRODUCTION,
json_loads: Optional[Callable] = None, json_loads: Optional[Callable[..., str]] = None,
json_dumps: Optional[Callable] = None, json_dumps: Optional[Callable[..., str]] = None,
): ):
super(AiohttpSession, self).__init__(api=api, json_loads=json_loads, json_dumps=json_dumps) super(AiohttpSession, self).__init__(api=api, json_loads=json_loads, json_dumps=json_dumps)
self._session: Optional[ClientSession] = None self._session: Optional[ClientSession] = None
@ -27,11 +27,11 @@ class AiohttpSession(BaseSession):
return self._session return self._session
async def close(self): async def close(self) -> None:
if self._session is not None and not self._session.closed: if self._session is not None and not self._session.closed:
await self._session.close() await self._session.close()
def build_form_data(self, request: Request): def build_form_data(self, request: Request) -> FormData:
form = FormData(quote_fields=False) form = FormData(quote_fields=False)
for key, value in request.data.items(): for key, value in request.data.items():
if value is None: if value is None:

View file

@ -3,7 +3,16 @@ from __future__ import annotations
import abc import abc
import datetime import datetime
import json import json
from typing import Any, AsyncGenerator, Callable, Optional, TypeVar, Union from types import TracebackType
from typing import (
Any,
AsyncGenerator,
Callable,
Optional,
Type,
TypeVar,
Union,
)
from aiogram.utils.exceptions import TelegramAPIError from aiogram.utils.exceptions import TelegramAPIError
@ -17,8 +26,8 @@ class BaseSession(abc.ABC):
def __init__( def __init__(
self, self,
api: Optional[TelegramAPIServer] = None, api: Optional[TelegramAPIServer] = None,
json_loads: Optional[Callable[[Any], Any]] = None, json_loads: Optional[Callable[..., str]] = None,
json_dumps: Optional[Callable[[Any], Any]] = None, json_dumps: Optional[Callable[..., str]] = None,
) -> None: ) -> None:
if api is None: if api is None:
api = PRODUCTION api = PRODUCTION
@ -37,7 +46,7 @@ class BaseSession(abc.ABC):
raise TelegramAPIError(response.description) raise TelegramAPIError(response.description)
@abc.abstractmethod @abc.abstractmethod
async def close(self): # pragma: no cover async def close(self) -> None: # pragma: no cover
pass pass
@abc.abstractmethod @abc.abstractmethod
@ -73,5 +82,10 @@ class BaseSession(abc.ABC):
async def __aenter__(self) -> BaseSession: async def __aenter__(self) -> BaseSession:
return self return self
async def __aexit__(self, exc_type, exc_val, exc_tb): async def __aexit__(
self,
exc_type: Optional[Type[BaseException]],
exc_value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
await self.close() await self.close()

View file

@ -2,7 +2,16 @@ from __future__ import annotations
import abc import abc
import secrets import secrets
from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Type, TypeVar, Union from typing import (
Generator,
TYPE_CHECKING,
Any,
Dict,
Generic,
Optional,
TypeVar,
Union,
)
from pydantic import BaseConfig, BaseModel, Extra from pydantic import BaseConfig, BaseModel, Extra
from pydantic.generics import GenericModel from pydantic.generics import GenericModel
@ -24,7 +33,7 @@ class Request(BaseModel):
class Config(BaseConfig): class Config(BaseConfig):
arbitrary_types_allowed = True arbitrary_types_allowed = True
def render_webhook_request(self): def render_webhook_request(self) -> Dict[str, Any]:
return { return {
"method": self.method, "method": self.method,
**{key: value for key, value in self.data.items() if value is not None}, **{key: value for key, value in self.data.items() if value is not None},
@ -48,7 +57,7 @@ class TelegramMethod(abc.ABC, BaseModel, Generic[T]):
@property @property
@abc.abstractmethod @abc.abstractmethod
def __returning__(self) -> Type: # pragma: no cover def __returning__(self) -> type: # pragma: no cover
pass pass
@abc.abstractmethod @abc.abstractmethod
@ -62,14 +71,14 @@ class TelegramMethod(abc.ABC, BaseModel, Generic[T]):
async def emit(self, bot: Bot) -> T: async def emit(self, bot: Bot) -> T:
return await bot(self) return await bot(self)
def __await__(self): def __await__(self) -> Generator[Any, None, T]:
from aiogram.api.client.bot import Bot from aiogram.api.client.bot import Bot
bot = Bot.get_current(no_error=False) bot = Bot.get_current(no_error=False)
return self.emit(bot).__await__() return self.emit(bot).__await__()
def prepare_file(name: str, value: Any, data: Dict[str, Any], files: Dict[str, Any]): def prepare_file(name: str, value: Any, data: Dict[str, Any], files: Dict[str, Any]) -> None:
if not value: if not value:
return return
if name == "thumb": if name == "thumb":
@ -101,7 +110,7 @@ def prepare_media_file(data: Dict[str, Any], files: Dict[str, InputFile]) -> Non
and isinstance(data["media"]["media"], InputFile) and isinstance(data["media"]["media"], InputFile)
): ):
tag = secrets.token_urlsafe(10) tag = secrets.token_urlsafe(10)
files[tag] = data["media"].pop("media") # type: ignore files[tag] = data["media"].pop("media")
data["media"]["media"] = f"attach://{tag}" data["media"]["media"] = f"attach://{tag}"

View file

@ -5,7 +5,7 @@ from pydantic import BaseModel, Extra
from aiogram.utils.mixins import ContextInstanceMixin from aiogram.utils.mixins import ContextInstanceMixin
class TelegramObject(ContextInstanceMixin, BaseModel): class TelegramObject(ContextInstanceMixin["TelegramObject"], BaseModel):
class Config: class Config:
use_enum_values = True use_enum_values = True
orm_mode = True orm_mode = True

View file

@ -4,7 +4,13 @@ import io
import os import os
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from pathlib import Path from pathlib import Path
from typing import AsyncGenerator, Optional, Union from typing import (
AsyncGenerator,
AsyncIterator,
Iterator,
Optional,
Union,
)
import aiofiles as aiofiles import aiofiles as aiofiles
@ -24,14 +30,14 @@ class InputFile(ABC):
self.chunk_size = chunk_size self.chunk_size = chunk_size
@classmethod @classmethod
def __get_validators__(cls): def __get_validators__(cls) -> Iterator[None]:
yield yield None
@abstractmethod @abstractmethod
async def read(self, chunk_size: int) -> AsyncGenerator[bytes, None]: # pragma: no cover async def read(self, chunk_size: int) -> AsyncGenerator[bytes, None]: # pragma: no cover
yield b"" yield b""
async def __aiter__(self): async def __aiter__(self) -> AsyncIterator[bytes]:
async for chunk in self.read(self.chunk_size): async for chunk in self.read(self.chunk_size):
yield chunk yield chunk

View file

@ -181,7 +181,7 @@ class Message(TelegramObject):
buttons.""" buttons."""
@property @property
def content_type(self): def content_type(self) -> str:
if self.text: if self.text:
return ContentType.TEXT return ContentType.TEXT
if self.audio: if self.audio:

View file

@ -32,7 +32,7 @@ class User(TelegramObject):
"""True, if the bot supports inline queries. Returned only in getMe.""" """True, if the bot supports inline queries. Returned only in getMe."""
@property @property
def full_name(self): def full_name(self) -> str:
if self.last_name: if self.last_name:
return f"{self.first_name} {self.last_name}" return f"{self.first_name} {self.last_name}"
return self.first_name return self.first_name

View file

@ -1,3 +1,5 @@
from __future__ import annotations
import asyncio import asyncio
import contextvars import contextvars
import warnings import warnings
@ -96,7 +98,7 @@ class Dispatcher(Router):
update_id = update.update_id + 1 update_id = update.update_id + 1
@classmethod @classmethod
async def _silent_call_request(cls, bot: Bot, result: TelegramMethod) -> None: async def _silent_call_request(cls, bot: Bot, result: TelegramMethod[Any]) -> None:
""" """
Simulate answer into WebHook Simulate answer into WebHook
@ -172,7 +174,7 @@ class Dispatcher(Router):
raise raise
async def feed_webhook_update( async def feed_webhook_update(
self, bot: Bot, update: Union[Update, Dict[str, Any]], _timeout: int = 55, **kwargs self, bot: Bot, update: Union[Update, Dict[str, Any]], _timeout: int = 55, **kwargs: Any
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
if not isinstance(update, Update): # Allow to use raw updates if not isinstance(update, Update): # Allow to use raw updates
update = Update(**update) update = Update(**update)
@ -181,18 +183,18 @@ class Dispatcher(Router):
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
waiter = loop.create_future() waiter = loop.create_future()
def release_waiter(*args: Any): def release_waiter(*args: Any) -> None:
if not waiter.done(): if not waiter.done():
waiter.set_result(None) waiter.set_result(None)
timeout_handle = loop.call_later(_timeout, release_waiter) timeout_handle = loop.call_later(_timeout, release_waiter)
process_updates: Future = asyncio.ensure_future( process_updates: Future[Any] = asyncio.ensure_future(
self._feed_webhook_update(bot=bot, update=update, **kwargs) self._feed_webhook_update(bot=bot, update=update, **kwargs)
) )
process_updates.add_done_callback(release_waiter, context=ctx) process_updates.add_done_callback(release_waiter, context=ctx)
def process_response(task: Future): def process_response(task: Future[Any]) -> None:
warnings.warn( warnings.warn(
f"Detected slow response into webhook.\n" f"Detected slow response into webhook.\n"
f"Telegram is waiting for response only first 60 seconds and then re-send update.\n" f"Telegram is waiting for response only first 60 seconds and then re-send update.\n"

View file

@ -1,7 +1,17 @@
import inspect import inspect
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import partial from functools import partial
from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple, Union from typing import (
Any,
Awaitable,
Callable,
Dict,
List,
Optional,
Tuple,
Union,
Type,
)
from aiogram.dispatcher.filters.base import BaseFilter from aiogram.dispatcher.filters.base import BaseFilter
from aiogram.dispatcher.handler.base import BaseHandler from aiogram.dispatcher.handler.base import BaseHandler
@ -10,7 +20,7 @@ CallbackType = Callable[[Any], Awaitable[Any]]
SyncFilter = Callable[[Any], Any] SyncFilter = Callable[[Any], Any]
AsyncFilter = Callable[[Any], Awaitable[Any]] AsyncFilter = Callable[[Any], Awaitable[Any]]
FilterType = Union[SyncFilter, AsyncFilter, BaseFilter] FilterType = Union[SyncFilter, AsyncFilter, BaseFilter]
HandlerType = Union[FilterType, BaseHandler] HandlerType = Union[FilterType, Type[BaseHandler]]
@dataclass @dataclass
@ -19,20 +29,20 @@ class CallableMixin:
awaitable: bool = field(init=False) awaitable: bool = field(init=False)
spec: inspect.FullArgSpec = field(init=False) spec: inspect.FullArgSpec = field(init=False)
def __post_init__(self): def __post_init__(self) -> None:
callback = self.callback callback = self.callback
self.awaitable = inspect.isawaitable(callback) or inspect.iscoroutinefunction(callback) self.awaitable = inspect.isawaitable(callback) or inspect.iscoroutinefunction(callback)
while hasattr(callback, "__wrapped__"): # Try to resolve decorated callbacks while hasattr(callback, "__wrapped__"): # Try to resolve decorated callbacks
callback = callback.__wrapped__ callback = callback.__wrapped__ # type: ignore
self.spec = inspect.getfullargspec(callback) self.spec = inspect.getfullargspec(callback)
def _prepare_kwargs(self, kwargs): def _prepare_kwargs(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
if self.spec.varkw: if self.spec.varkw:
return kwargs return kwargs
return {k: v for k, v in kwargs.items() if k in self.spec.args} return {k: v for k, v in kwargs.items() if k in self.spec.args}
async def call(self, *args, **kwargs): async def call(self, *args: Any, **kwargs: Any) -> Any:
wrapped = partial(self.callback, *args, **self._prepare_kwargs(kwargs)) wrapped = partial(self.callback, *args, **self._prepare_kwargs(kwargs))
if self.awaitable: if self.awaitable:
return await wrapped() return await wrapped()
@ -49,10 +59,9 @@ class HandlerObject(CallableMixin):
callback: HandlerType callback: HandlerType
filters: Optional[List[FilterObject]] = None filters: Optional[List[FilterObject]] = None
def __post_init__(self): def __post_init__(self) -> None:
super(HandlerObject, self).__post_init__() super(HandlerObject, self).__post_init__()
if inspect.isclass(self.callback) and issubclass(self.callback, BaseHandler): # type: ignore
if inspect.isclass(self.callback) and issubclass(self.callback, BaseHandler):
self.awaitable = True self.awaitable = True
async def check(self, *args: Any, **kwargs: Any) -> Tuple[bool, Dict[str, Any]]: async def check(self, *args: Any, **kwargs: Any) -> Tuple[bool, Dict[str, Any]]:

View file

@ -1,4 +1,8 @@
from typing import Dict, Tuple, Union from typing import (
Dict,
Tuple,
Type,
)
from .base import BaseFilter from .base import BaseFilter
from .command import Command, CommandObject from .command import Command, CommandObject
@ -14,7 +18,7 @@ __all__ = (
"ContentTypesFilter", "ContentTypesFilter",
) )
BUILTIN_FILTERS: Dict[str, Union[Tuple[BaseFilter], Tuple]] = { BUILTIN_FILTERS: Dict[str, Tuple[Type[BaseFilter], ...]] = {
"update": (), "update": (),
"message": (Text, Command, ContentTypesFilter), "message": (Text, Command, ContentTypesFilter),
"edited_message": (Text, Command, ContentTypesFilter), "edited_message": (Text, Command, ContentTypesFilter),

View file

@ -1,5 +1,12 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Dict, Union from typing import (
TYPE_CHECKING,
Any,
Dict,
Union,
Callable,
Awaitable,
)
from pydantic import BaseModel from pydantic import BaseModel
@ -9,14 +16,13 @@ class BaseFilter(ABC, BaseModel):
# This checking type-hint is needed because mypy checks validity of overrides and raises: # This checking type-hint is needed because mypy checks validity of overrides and raises:
# error: Signature of "__call__" incompatible with supertype "BaseFilter" [override] # error: Signature of "__call__" incompatible with supertype "BaseFilter" [override]
# https://mypy.readthedocs.io/en/latest/error_code_list.html#check-validity-of-overrides-override # https://mypy.readthedocs.io/en/latest/error_code_list.html#check-validity-of-overrides-override
__call__: Callable[..., Awaitable[Union[bool, Dict[str, Any]]]]
pass
else: # pragma: no cover else: # pragma: no cover
@abstractmethod @abstractmethod
async def __call__(self, *args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]: async def __call__(self, *args: Any, **kwargs: Any) -> Union[bool, Dict[str, Any]]:
pass pass
def __await__(self): # pragma: no cover def __await__(self): # type: ignore # pragma: no cover
# Is needed only for inspection and this method is never be called # Is needed only for inspection and this method is never be called
return self.__call__ return self.__call__

View file

@ -10,7 +10,7 @@ from aiogram import Bot
from aiogram.api.types import Message from aiogram.api.types import Message
from aiogram.dispatcher.filters import BaseFilter from aiogram.dispatcher.filters import BaseFilter
CommandPatterType = Union[str, re.Pattern] # type: ignore CommandPatterType = Union[str, re.Pattern]
class Command(BaseFilter): class Command(BaseFilter):

View file

@ -80,7 +80,7 @@ class Text(BaseFilter):
# Impossible because the validator prevents this situation # Impossible because the validator prevents this situation
return False # pragma: no cover return False # pragma: no cover
def prepare_text(self, text: str): def prepare_text(self, text: str) -> str:
if self.text_ignore_case: if self.text_ignore_case:
return str(text).lower() return str(text).lower()
else: else:

View file

@ -1,5 +1,12 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Dict, Generic, TypeVar from typing import (
TYPE_CHECKING,
Any,
Dict,
Generic,
TypeVar,
cast,
)
from aiogram import Bot from aiogram import Bot
from aiogram.api.types import Update from aiogram.api.types import Update
@ -25,16 +32,16 @@ class BaseHandler(BaseHandlerMixin[T], ABC):
@property @property
def bot(self) -> Bot: def bot(self) -> Bot:
if "bot" in self.data: if "bot" in self.data:
return self.data["bot"] return cast(Bot, self.data["bot"])
return Bot.get_current() return Bot.get_current(no_error=False)
@property @property
def update(self) -> Update: def update(self) -> Update:
return self.data["update"] return cast(Update, self.data["update"])
@abstractmethod @abstractmethod
async def handle(self) -> Any: # pragma: no cover async def handle(self) -> Any: # pragma: no cover
pass pass
def __await__(self): def __await__(self) -> Any:
return self.handle().__await__() return self.handle().__await__()

View file

@ -1,5 +1,8 @@
from abc import ABC from abc import ABC
from typing import Optional from typing import (
Optional,
cast,
)
from aiogram.api.types import Chat, Message, User from aiogram.api.types import Chat, Message, User
from aiogram.dispatcher.filters import CommandObject from aiogram.dispatcher.filters import CommandObject
@ -24,5 +27,5 @@ class MessageHandlerCommandMixin(BaseHandlerMixin[Message]):
@property @property
def command(self) -> Optional[CommandObject]: def command(self) -> Optional[CommandObject]:
if "command" in self.data: if "command" in self.data:
return self.data["command"] return cast(CommandObject, self.data["command"])
return None return None

View file

@ -3,7 +3,12 @@ from __future__ import annotations
import warnings import warnings
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union
from ..api.types import Chat, Update, User from ..api.types import (
Chat,
TelegramObject,
Update,
User,
)
from ..utils.imports import import_module from ..utils.imports import import_module
from ..utils.warnings import CodeHasNoEffect from ..utils.warnings import CodeHasNoEffect
from .event.observer import EventObserver, SkipHandler, TelegramEventObserver from .event.observer import EventObserver, SkipHandler, TelegramEventObserver
@ -151,6 +156,7 @@ class Router:
chat: Optional[Chat] = None chat: Optional[Chat] = None
from_user: Optional[User] = None from_user: Optional[User] = None
event: TelegramObject
if update.message: if update.message:
update_type = "message" update_type = "message"
from_user = update.message.from_user from_user = update.message.from_user
@ -211,7 +217,7 @@ class Router:
raise SkipHandler raise SkipHandler
async def emit_startup(self, *args, **kwargs) -> None: async def emit_startup(self, *args: Any, **kwargs: Any) -> None:
""" """
Recursively call startup callbacks Recursively call startup callbacks
@ -225,7 +231,7 @@ class Router:
for router in self.sub_routers: for router in self.sub_routers:
await router.emit_startup(*args, **kwargs) await router.emit_startup(*args, **kwargs)
async def emit_shutdown(self, *args, **kwargs) -> None: async def emit_shutdown(self, *args: Any, **kwargs: Any) -> None:
""" """
Recursively call shutdown callbacks to graceful shutdown Recursively call shutdown callbacks to graceful shutdown

View file

@ -13,7 +13,16 @@ Example:
>>> print(MyHelper.all()) >>> print(MyHelper.all())
<<< ['barItem', 'bazItem', 'fooItem', 'lorem'] <<< ['barItem', 'bazItem', 'fooItem', 'lorem']
""" """
from typing import List import inspect
from typing import (
Any,
Callable,
Iterable,
List,
Optional,
Union,
cast,
)
PROPS_KEYS_ATTR_NAME = "_props_keys" PROPS_KEYS_ATTR_NAME = "_props_keys"
@ -22,12 +31,12 @@ class Helper:
mode = "" mode = ""
@classmethod @classmethod
def all(cls): def all(cls) -> List[Any]:
""" """
Get all consts Get all consts
:return: list :return: list
""" """
result = [] result: List[Any] = []
for name in dir(cls): for name in dir(cls):
if not name.isupper(): if not name.isupper():
continue continue
@ -49,7 +58,7 @@ class HelperMode(Helper):
lowercase = "lowercase" lowercase = "lowercase"
@classmethod @classmethod
def all(cls): def all(cls) -> List[str]:
return [ return [
cls.SCREAMING_SNAKE_CASE, cls.SCREAMING_SNAKE_CASE,
cls.lowerCamelCase, cls.lowerCamelCase,
@ -59,7 +68,7 @@ class HelperMode(Helper):
] ]
@classmethod @classmethod
def _screaming_snake_case(cls, text): def _screaming_snake_case(cls, text: str) -> str:
""" """
Transform text to SCREAMING_SNAKE_CASE Transform text to SCREAMING_SNAKE_CASE
@ -77,7 +86,7 @@ class HelperMode(Helper):
return result return result
@classmethod @classmethod
def _snake_case(cls, text): def _snake_case(cls, text: str) -> str:
""" """
Transform text to snake case (Based on SCREAMING_SNAKE_CASE) Transform text to snake case (Based on SCREAMING_SNAKE_CASE)
@ -89,7 +98,7 @@ class HelperMode(Helper):
return cls._screaming_snake_case(text).lower() return cls._screaming_snake_case(text).lower()
@classmethod @classmethod
def _camel_case(cls, text, first_upper=False): def _camel_case(cls, text: str, first_upper: bool = False) -> str:
""" """
Transform text to camelCase or CamelCase Transform text to camelCase or CamelCase
@ -113,7 +122,7 @@ class HelperMode(Helper):
return result return result
@classmethod @classmethod
def apply(cls, text, mode): def apply(cls, text: str, mode: Union[str, Callable[[str], str]]) -> str:
""" """
Apply mode for text Apply mode for text
@ -136,7 +145,20 @@ class HelperMode(Helper):
return text return text
class Item: class _BaseItem:
def __init__(self, value: Optional[str] = None):
self._value = cast(str, value)
def __set_name__(self, owner: Any, name: str) -> None:
if not name.isupper():
raise NameError("Name for helper item must be in uppercase!")
if not self._value:
if not inspect.isclass(owner) or not issubclass(owner, Helper):
raise RuntimeError("Instances of Item can be used only as Helper attributes")
self._value = HelperMode.apply(name, owner.mode)
class Item(_BaseItem):
""" """
Helper item Helper item
@ -144,34 +166,24 @@ class Item:
it will be automatically generated based on a variable's name it will be automatically generated based on a variable's name
""" """
def __init__(self, value=None): def __get__(self, instance: Any, owner: Any) -> str:
self._value = value
def __get__(self, instance, owner):
return self._value return self._value
def __set_name__(self, owner, name):
if not name.isupper():
raise NameError("Name for helper item must be in uppercase!")
if not self._value:
if hasattr(owner, "mode"):
self._value = HelperMode.apply(name, getattr(owner, "mode"))
class ListItem(_BaseItem):
class ListItem(Item):
""" """
This item is always a list This item is always a list
You can use &, | and + operators for that. You can use &, | and + operators for that.
""" """
def add(self, other): # pragma: no cover def add(self, other: "ListItem") -> "ListItem": # pragma: no cover
return self + other return self + other
def __get__(self, instance, owner): def __get__(self, instance: Any, owner: Any) -> "ItemsList":
return ItemsList(self._value) return ItemsList(self._value)
def __getitem__(self, item): # pragma: no cover def __getitem__(self, item: Any) -> Any: # pragma: no cover
# Only for IDE. This method is never be called. # Only for IDE. This method is never be called.
return self._value return self._value
@ -179,17 +191,17 @@ class ListItem(Item):
__iadd__ = __add__ = __rand__ = __and__ = __ror__ = __or__ = add __iadd__ = __add__ = __rand__ = __and__ = __ror__ = __or__ = add
class ItemsList(list): class ItemsList(List[str]):
""" """
Patch for default list Patch for default list
This class provides +, &, |, +=, &=, |= operators for extending the list This class provides +, &, |, +=, &=, |= operators for extending the list
""" """
def __init__(self, *seq): def __init__(self, *seq: Any):
super(ItemsList, self).__init__(map(str, seq)) super(ItemsList, self).__init__(map(str, seq))
def add(self, other): def add(self, other: Iterable[str]) -> "ItemsList":
self.extend(other) self.extend(other)
return self return self
@ -197,7 +209,7 @@ class ItemsList(list):
class OrderedHelperMeta(type): class OrderedHelperMeta(type):
def __new__(mcs, name, bases, namespace, **kwargs): def __new__(mcs, name: Any, bases: Any, namespace: Any, **kwargs: Any) -> "OrderedHelperMeta":
cls = super().__new__(mcs, name, bases, namespace) cls = super().__new__(mcs, name, bases, namespace)
props_keys = [] props_keys = []
@ -209,10 +221,11 @@ class OrderedHelperMeta(type):
setattr(cls, PROPS_KEYS_ATTR_NAME, props_keys) setattr(cls, PROPS_KEYS_ATTR_NAME, props_keys)
return cls # ref: https://gitter.im/python/typing?at=5da98cc5fa637359fc9cbfe1
return cast(OrderedHelperMeta, cls)
class OrderedHelper(metaclass=OrderedHelperMeta): class OrderedHelper(Helper, metaclass=OrderedHelperMeta):
mode = "" mode = ""
@classmethod @classmethod

View file

@ -1,51 +1,90 @@
from __future__ import annotations
import contextvars import contextvars
from typing import Type, TypeVar from typing import (
Any,
ClassVar,
Generic,
Optional,
TypeVar,
cast,
overload,
Dict,
)
__all__ = ("DataMixin", "ContextInstanceMixin") __all__ = ("ContextInstanceMixin", "DataMixin")
from typing_extensions import Literal
class DataMixin: class DataMixin:
@property @property
def data(self): def data(self) -> Dict[str, Any]:
data = getattr(self, "_data", None) data: Optional[Dict[str, Any]] = getattr(self, "_data", None)
if data is None: if data is None:
data = {} data = {}
setattr(self, "_data", data) setattr(self, "_data", data)
return data return data
def __getitem__(self, item): def __getitem__(self, key: str) -> Any:
return self.data[item] return self.data[key]
def __setitem__(self, key, value): def __setitem__(self, key: str, value: Any) -> None:
self.data[key] = value self.data[key] = value
def __delitem__(self, key): def __delitem__(self, key: str) -> None:
del self.data[key] del self.data[key]
def __contains__(self, item): def __contains__(self, key: str) -> bool:
return item in self.data return key in self.data
def get(self, key, default=None): def get(self, key: str, default: Optional[Any] = None) -> Optional[Any]:
return self.data.get(key, default) return self.data.get(key, default)
T = TypeVar("T") ContextInstance = TypeVar("ContextInstance")
class ContextInstanceMixin: class ContextInstanceMixin(Generic[ContextInstance]):
def __init_subclass__(cls, **kwargs): __context_instance: ClassVar[contextvars.ContextVar[ContextInstance]]
super().__init_subclass__(**kwargs)
def __init_subclass__(cls, **kwargs: Any) -> None:
super().__init_subclass__()
cls.__context_instance = contextvars.ContextVar(f"instance_{cls.__name__}") cls.__context_instance = contextvars.ContextVar(f"instance_{cls.__name__}")
return cls
@overload
@classmethod
def get_current(cls) -> Optional[ContextInstance]: # pragma: no cover
...
@overload # noqa: F811, it's overload, not redefinition
@classmethod
def get_current(cls, no_error: Literal[True]) -> Optional[ContextInstance]: # pragma: no cover # noqa: F811
...
@overload # noqa: F811, it's overload, not redefinition
@classmethod
def get_current(cls, no_error: Literal[False]) -> ContextInstance: # pragma: no cover # noqa: F811
...
@classmethod # noqa: F811, it's overload, not redefinition
def get_current(cls, no_error: bool = True) -> Optional[ContextInstance]: # pragma: no cover # noqa: F811
# on mypy 0.770 I catch that contextvars.ContextVar always contextvars.ContextVar[Any]
cls.__context_instance = cast(
contextvars.ContextVar[ContextInstance], cls.__context_instance
)
try:
current: Optional[ContextInstance] = cls.__context_instance.get()
except LookupError:
if no_error:
current = None
else:
raise
return current
@classmethod @classmethod
def get_current(cls: Type[T], no_error=True) -> T: def set_current(cls, value: ContextInstance) -> contextvars.Token[ContextInstance]:
if no_error:
return cls.__context_instance.get(None)
return cls.__context_instance.get()
@classmethod
def set_current(cls: Type[T], value: T) -> contextvars.Token:
if not isinstance(value, cls): if not isinstance(value, cls):
raise TypeError( raise TypeError(
f"Value should be instance of {cls.__name__!r} not {type(value).__name__!r}" f"Value should be instance of {cls.__name__!r} not {type(value).__name__!r}"
@ -53,5 +92,5 @@ class ContextInstanceMixin:
return cls.__context_instance.set(value) return cls.__context_instance.set(value)
@classmethod @classmethod
def reset_current(cls: Type[T], token: contextvars.Token): def reset_current(cls, token: contextvars.Token[ContextInstance]) -> None:
cls.__context_instance.reset(token) cls.__context_instance.reset(token)

View file

@ -1,7 +1,31 @@
[mypy] [mypy]
# plugins = pydantic.mypy ;plugins = pydantic.mypy
python_version = 3.7
ignore_missing_imports = True
show_error_context = True
show_error_codes = True show_error_codes = True
show_error_context = True
pretty = 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

260
poetry.lock generated
View file

@ -153,8 +153,8 @@ category = "dev"
description = "Composable command line interface toolkit" description = "Composable command line interface toolkit"
name = "click" name = "click"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "7.0" version = "7.1.1"
[[package]] [[package]]
category = "dev" category = "dev"
@ -171,7 +171,7 @@ description = "Code coverage measurement for Python"
name = "coverage" name = "coverage"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
version = "5.0.3" version = "5.0.4"
[package.extras] [package.extras]
toml = ["toml"] toml = ["toml"]
@ -182,7 +182,7 @@ description = "Decorators for Humans"
name = "decorator" name = "decorator"
optional = false optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*" python-versions = ">=2.6, !=3.0.*, !=3.1.*"
version = "4.4.1" version = "4.4.2"
[[package]] [[package]]
category = "dev" category = "dev"
@ -219,6 +219,14 @@ flake8 = ">=3.3.0"
jinja2 = ">=2.9.0" jinja2 = ">=2.9.0"
pygments = ">=2.2.0" pygments = ">=2.2.0"
[[package]]
category = "dev"
description = "Clean single-source support for Python 3 and 2"
name = "future"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
version = "0.18.2"
[[package]] [[package]]
category = "main" category = "main"
description = "Internationalized Domain Names in Applications (IDNA)" description = "Internationalized Domain Names in Applications (IDNA)"
@ -249,7 +257,7 @@ description = "IPython: Productive Interactive Computing"
name = "ipython" name = "ipython"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
version = "7.12.0" version = "7.13.0"
[package.dependencies] [package.dependencies]
appnope = "*" appnope = "*"
@ -265,7 +273,7 @@ setuptools = ">=18.5"
traitlets = ">=4.2" traitlets = ">=4.2"
[package.extras] [package.extras]
all = ["ipyparallel", "requests", "notebook", "qtconsole", "ipywidgets", "pygments", "nbconvert", "testpath", "Sphinx (>=1.3)", "nbformat", "numpy (>=1.14)", "ipykernel", "nose (>=0.10.1)"] all = ["numpy (>=1.14)", "testpath", "notebook", "nose (>=0.10.1)", "nbconvert", "requests", "ipywidgets", "qtconsole", "ipyparallel", "Sphinx (>=1.3)", "pygments", "nbformat", "ipykernel"]
doc = ["Sphinx (>=1.3)"] doc = ["Sphinx (>=1.3)"]
kernel = ["ipykernel"] kernel = ["ipykernel"]
nbconvert = ["nbconvert"] nbconvert = ["nbconvert"]
@ -338,6 +346,25 @@ version = "2.6.1"
six = "*" six = "*"
tornado = "*" tornado = "*"
[[package]]
category = "dev"
description = "A Python implementation of Lunr.js"
name = "lunr"
optional = false
python-versions = "*"
version = "0.5.6"
[package.dependencies]
future = ">=0.16.0"
six = ">=1.11.0"
[package.dependencies.nltk]
optional = true
version = ">=3.2.5"
[package.extras]
languages = ["nltk (>=3.2.5)"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
@ -406,17 +433,21 @@ category = "dev"
description = "Project documentation with Markdown." description = "Project documentation with Markdown."
name = "mkdocs" name = "mkdocs"
optional = false optional = false
python-versions = ">=2.7.9,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" python-versions = ">=3.5"
version = "1.0.4" version = "1.1"
[package.dependencies] [package.dependencies]
Jinja2 = ">=2.7.1" Jinja2 = ">=2.10.1"
Markdown = ">=2.3.1" Markdown = ">=3.2.1"
PyYAML = ">=3.10" PyYAML = ">=3.10"
click = ">=3.3" click = ">=3.3"
livereload = ">=2.5.1" livereload = ">=2.5.1"
tornado = ">=5.0" tornado = ">=5.0"
[package.dependencies.lunr]
extras = ["languages"]
version = "0.5.6"
[[package]] [[package]]
category = "dev" category = "dev"
description = "A Material Design theme for MkDocs" description = "A Material Design theme for MkDocs"
@ -453,7 +484,7 @@ description = "Optional static typing for Python"
name = "mypy" name = "mypy"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "0.761" version = "0.770"
[package.dependencies] [package.dependencies]
mypy-extensions = ">=0.4.3,<0.5.0" mypy-extensions = ">=0.4.3,<0.5.0"
@ -471,13 +502,32 @@ optional = false
python-versions = "*" python-versions = "*"
version = "0.4.3" version = "0.4.3"
[[package]]
category = "dev"
description = "Natural Language Toolkit"
name = "nltk"
optional = false
python-versions = "*"
version = "3.4.5"
[package.dependencies]
six = "*"
[package.extras]
all = ["pyparsing", "scikit-learn", "python-crfsuite", "matplotlib", "scipy", "gensim", "requests", "twython", "numpy"]
corenlp = ["requests"]
machine_learning = ["gensim", "numpy", "python-crfsuite", "scikit-learn", "scipy"]
plot = ["matplotlib"]
tgrep = ["pyparsing"]
twitter = ["twython"]
[[package]] [[package]]
category = "dev" category = "dev"
description = "Core utilities for Python packages" description = "Core utilities for Python packages"
name = "packaging" name = "packaging"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "20.1" version = "20.3"
[package.dependencies] [package.dependencies]
pyparsing = ">=2.0.2" pyparsing = ">=2.0.2"
@ -489,7 +539,7 @@ description = "A Python Parser"
name = "parso" name = "parso"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
version = "0.6.1" version = "0.6.2"
[package.extras] [package.extras]
testing = ["docopt", "pytest (>=3.0.7)"] testing = ["docopt", "pytest (>=3.0.7)"]
@ -543,8 +593,8 @@ category = "dev"
description = "Library for building powerful interactive command lines in Python" description = "Library for building powerful interactive command lines in Python"
name = "prompt-toolkit" name = "prompt-toolkit"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6.1"
version = "3.0.3" version = "3.0.4"
[package.dependencies] [package.dependencies]
wcwidth = "*" wcwidth = "*"
@ -600,8 +650,8 @@ category = "dev"
description = "Pygments is a syntax highlighting package written in Python." description = "Pygments is a syntax highlighting package written in Python."
name = "pygments" name = "pygments"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=3.5"
version = "2.5.2" version = "2.6.1"
[[package]] [[package]]
category = "dev" category = "dev"
@ -628,7 +678,7 @@ description = "pytest: simple powerful testing with Python"
name = "pytest" name = "pytest"
optional = false optional = false
python-versions = ">=3.5" python-versions = ">=3.5"
version = "5.3.5" version = "5.4.1"
[package.dependencies] [package.dependencies]
atomicwrites = ">=1.0" atomicwrites = ">=1.0"
@ -683,7 +733,7 @@ description = "pytest plugin for generating HTML reports"
name = "pytest-html" name = "pytest-html"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
version = "2.0.1" version = "2.1.0"
[package.dependencies] [package.dependencies]
pytest = ">=5.0" pytest = ">=5.0"
@ -781,7 +831,7 @@ description = "Tornado is a Python web framework and asynchronous networking lib
name = "tornado" name = "tornado"
optional = false optional = false
python-versions = ">= 3.5" python-versions = ">= 3.5"
version = "6.0.3" version = "6.0.4"
[[package]] [[package]]
category = "dev" category = "dev"
@ -851,7 +901,7 @@ marker = "python_version >= \"3.5\" and python_version < \"3.8\" or python_versi
name = "zipp" name = "zipp"
optional = false optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
version = "3.0.0" version = "3.1.0"
[package.extras] [package.extras]
docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
@ -861,7 +911,7 @@ testing = ["jaraco.itertools", "func-timeout"]
fast = ["uvloop"] fast = ["uvloop"]
[metadata] [metadata]
content-hash = "2eb50b5b57d0fac4780f1eb3f92ff129d891fd346e0c00856c1a56c58feffb03" content-hash = "bce1bc7bf9eb949283094490a084d484a3d2f7b0d992ea3a4ea1e75401f6e2da"
python-versions = "^3.7" python-versions = "^3.7"
[metadata.files] [metadata.files]
@ -931,49 +981,49 @@ chardet = [
{file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"},
] ]
click = [ click = [
{file = "Click-7.0-py2.py3-none-any.whl", hash = "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13"}, {file = "click-7.1.1-py2.py3-none-any.whl", hash = "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"},
{file = "Click-7.0.tar.gz", hash = "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"}, {file = "click-7.1.1.tar.gz", hash = "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc"},
] ]
colorama = [ colorama = [
{file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"}, {file = "colorama-0.4.3-py2.py3-none-any.whl", hash = "sha256:7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff"},
{file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"}, {file = "colorama-0.4.3.tar.gz", hash = "sha256:e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"},
] ]
coverage = [ coverage = [
{file = "coverage-5.0.3-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:cc1109f54a14d940b8512ee9f1c3975c181bbb200306c6d8b87d93376538782f"}, {file = "coverage-5.0.4-cp27-cp27m-macosx_10_12_x86_64.whl", hash = "sha256:8a620767b8209f3446197c0e29ba895d75a1e272a36af0786ec70fe7834e4307"},
{file = "coverage-5.0.3-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:be18f4ae5a9e46edae3f329de2191747966a34a3d93046dbdf897319923923bc"}, {file = "coverage-5.0.4-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:73aa6e86034dad9f00f4bbf5a666a889d17d79db73bc5af04abd6c20a014d9c8"},
{file = "coverage-5.0.3-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:3230d1003eec018ad4a472d254991e34241e0bbd513e97a29727c7c2f637bd2a"}, {file = "coverage-5.0.4-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:408ce64078398b2ee2ec08199ea3fcf382828d2f8a19c5a5ba2946fe5ddc6c31"},
{file = "coverage-5.0.3-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:e69215621707119c6baf99bda014a45b999d37602cb7043d943c76a59b05bf52"}, {file = "coverage-5.0.4-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:cda33311cb9fb9323958a69499a667bd728a39a7aa4718d7622597a44c4f1441"},
{file = "coverage-5.0.3-cp27-cp27m-win32.whl", hash = "sha256:1daa3eceed220f9fdb80d5ff950dd95112cd27f70d004c7918ca6dfc6c47054c"}, {file = "coverage-5.0.4-cp27-cp27m-win32.whl", hash = "sha256:5f587dfd83cb669933186661a351ad6fc7166273bc3e3a1531ec5c783d997aac"},
{file = "coverage-5.0.3-cp27-cp27m-win_amd64.whl", hash = "sha256:51bc7710b13a2ae0c726f69756cf7ffd4362f4ac36546e243136187cfcc8aa73"}, {file = "coverage-5.0.4-cp27-cp27m-win_amd64.whl", hash = "sha256:9fad78c13e71546a76c2f8789623eec8e499f8d2d799f4b4547162ce0a4df435"},
{file = "coverage-5.0.3-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:9bea19ac2f08672636350f203db89382121c9c2ade85d945953ef3c8cf9d2a68"}, {file = "coverage-5.0.4-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:2e08c32cbede4a29e2a701822291ae2bc9b5220a971bba9d1e7615312efd3037"},
{file = "coverage-5.0.3-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:5012d3b8d5a500834783689a5d2292fe06ec75dc86ee1ccdad04b6f5bf231691"}, {file = "coverage-5.0.4-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:922fb9ef2c67c3ab20e22948dcfd783397e4c043a5c5fa5ff5e9df5529074b0a"},
{file = "coverage-5.0.3-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:d513cc3db248e566e07a0da99c230aca3556d9b09ed02f420664e2da97eac301"}, {file = "coverage-5.0.4-cp35-cp35m-macosx_10_12_x86_64.whl", hash = "sha256:c3fc325ce4cbf902d05a80daa47b645d07e796a80682c1c5800d6ac5045193e5"},
{file = "coverage-5.0.3-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:3dbb72eaeea5763676a1a1efd9b427a048c97c39ed92e13336e726117d0b72bf"}, {file = "coverage-5.0.4-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:046a1a742e66d065d16fb564a26c2a15867f17695e7f3d358d7b1ad8a61bca30"},
{file = "coverage-5.0.3-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:15cf13a6896048d6d947bf7d222f36e4809ab926894beb748fc9caa14605d9c3"}, {file = "coverage-5.0.4-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6ad6ca45e9e92c05295f638e78cd42bfaaf8ee07878c9ed73e93190b26c125f7"},
{file = "coverage-5.0.3-cp35-cp35m-win32.whl", hash = "sha256:fca1669d464f0c9831fd10be2eef6b86f5ebd76c724d1e0706ebdff86bb4adf0"}, {file = "coverage-5.0.4-cp35-cp35m-win32.whl", hash = "sha256:eda55e6e9ea258f5e4add23bcf33dc53b2c319e70806e180aecbff8d90ea24de"},
{file = "coverage-5.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:1e44a022500d944d42f94df76727ba3fc0a5c0b672c358b61067abb88caee7a0"}, {file = "coverage-5.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:4a8a259bf990044351baf69d3b23e575699dd60b18460c71e81dc565f5819ac1"},
{file = "coverage-5.0.3-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:b26aaf69713e5674efbde4d728fb7124e429c9466aeaf5f4a7e9e699b12c9fe2"}, {file = "coverage-5.0.4-cp36-cp36m-macosx_10_13_x86_64.whl", hash = "sha256:f372cdbb240e09ee855735b9d85e7f50730dcfb6296b74b95a3e5dea0615c4c1"},
{file = "coverage-5.0.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:722e4557c8039aad9592c6a4213db75da08c2cd9945320220634f637251c3894"}, {file = "coverage-5.0.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a37c6233b28e5bc340054cf6170e7090a4e85069513320275a4dc929144dccf0"},
{file = "coverage-5.0.3-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:7afad9835e7a651d3551eab18cbc0fdb888f0a6136169fbef0662d9cdc9987cf"}, {file = "coverage-5.0.4-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:443be7602c790960b9514567917af538cac7807a7c0c0727c4d2bbd4014920fd"},
{file = "coverage-5.0.3-cp36-cp36m-win32.whl", hash = "sha256:25dbf1110d70bab68a74b4b9d74f30e99b177cde3388e07cc7272f2168bd1477"}, {file = "coverage-5.0.4-cp36-cp36m-win32.whl", hash = "sha256:165a48268bfb5a77e2d9dbb80de7ea917332a79c7adb747bd005b3a07ff8caf0"},
{file = "coverage-5.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:c312e57847db2526bc92b9bfa78266bfbaabac3fdcd751df4d062cd4c23e46dc"}, {file = "coverage-5.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:0a907199566269e1cfa304325cc3b45c72ae341fbb3253ddde19fa820ded7a8b"},
{file = "coverage-5.0.3-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:a8b8ac7876bc3598e43e2603f772d2353d9931709345ad6c1149009fd1bc81b8"}, {file = "coverage-5.0.4-cp37-cp37m-macosx_10_13_x86_64.whl", hash = "sha256:513e6526e0082c59a984448f4104c9bf346c2da9961779ede1fc458e8e8a1f78"},
{file = "coverage-5.0.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:527b4f316e6bf7755082a783726da20671a0cc388b786a64417780b90565b987"}, {file = "coverage-5.0.4-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:3844c3dab800ca8536f75ae89f3cf566848a3eb2af4d9f7b1103b4f4f7a5dad6"},
{file = "coverage-5.0.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:d649dc0bcace6fcdb446ae02b98798a856593b19b637c1b9af8edadf2b150bea"}, {file = "coverage-5.0.4-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:641e329e7f2c01531c45c687efcec8aeca2a78a4ff26d49184dce3d53fc35014"},
{file = "coverage-5.0.3-cp37-cp37m-win32.whl", hash = "sha256:cd60f507c125ac0ad83f05803063bed27e50fa903b9c2cfee3f8a6867ca600fc"}, {file = "coverage-5.0.4-cp37-cp37m-win32.whl", hash = "sha256:db1d4e38c9b15be1521722e946ee24f6db95b189d1447fa9ff18dd16ba89f732"},
{file = "coverage-5.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c60097190fe9dc2b329a0eb03393e2e0829156a589bd732e70794c0dd804258e"}, {file = "coverage-5.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:62061e87071497951155cbccee487980524d7abea647a1b2a6eb6b9647df9006"},
{file = "coverage-5.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:d7008a6796095a79544f4da1ee49418901961c97ca9e9d44904205ff7d6aa8cb"}, {file = "coverage-5.0.4-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:65a7e00c00472cd0f59ae09d2fb8a8aaae7f4a0cf54b2b74f3138d9f9ceb9cb2"},
{file = "coverage-5.0.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ea9525e0fef2de9208250d6c5aeeee0138921057cd67fcef90fbed49c4d62d37"}, {file = "coverage-5.0.4-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1f66cf263ec77af5b8fe14ef14c5e46e2eb4a795ac495ad7c03adc72ae43fafe"},
{file = "coverage-5.0.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c62a2143e1313944bf4a5ab34fd3b4be15367a02e9478b0ce800cb510e3bbb9d"}, {file = "coverage-5.0.4-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:85596aa5d9aac1bf39fe39d9fa1051b0f00823982a1de5766e35d495b4a36ca9"},
{file = "coverage-5.0.3-cp38-cp38m-win32.whl", hash = "sha256:b0840b45187699affd4c6588286d429cd79a99d509fe3de0f209594669bb0954"}, {file = "coverage-5.0.4-cp38-cp38-win32.whl", hash = "sha256:86a0ea78fd851b313b2e712266f663e13b6bc78c2fb260b079e8b67d970474b1"},
{file = "coverage-5.0.3-cp38-cp38m-win_amd64.whl", hash = "sha256:76e2057e8ffba5472fd28a3a010431fd9e928885ff480cb278877c6e9943cc2e"}, {file = "coverage-5.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:03f630aba2b9b0d69871c2e8d23a69b7fe94a1e2f5f10df5049c0df99db639a0"},
{file = "coverage-5.0.3-cp39-cp39m-win32.whl", hash = "sha256:b63dd43f455ba878e5e9f80ba4f748c0a2156dde6e0e6e690310e24d6e8caf40"}, {file = "coverage-5.0.4-cp39-cp39-win32.whl", hash = "sha256:7c9762f80a25d8d0e4ab3cb1af5d9dffbddb3ee5d21c43e3474c84bf5ff941f7"},
{file = "coverage-5.0.3-cp39-cp39m-win_amd64.whl", hash = "sha256:da93027835164b8223e8e5af2cf902a4c80ed93cb0909417234f4a9df3bcd9af"}, {file = "coverage-5.0.4-cp39-cp39-win_amd64.whl", hash = "sha256:4482f69e0701139d0f2c44f3c395d1d1d37abd81bfafbf9b6efbe2542679d892"},
{file = "coverage-5.0.3.tar.gz", hash = "sha256:77afca04240c40450c331fa796b3eab6f1e15c5ecf8bf2b8bee9706cd5452fef"}, {file = "coverage-5.0.4.tar.gz", hash = "sha256:1b60a95fc995649464e0cd48cecc8288bac5f4198f21d04b8229dc4097d76823"},
] ]
decorator = [ decorator = [
{file = "decorator-4.4.1-py2.py3-none-any.whl", hash = "sha256:5d19b92a3c8f7f101c8dd86afd86b0f061a8ce4540ab8cd401fa2542756bce6d"}, {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"},
{file = "decorator-4.4.1.tar.gz", hash = "sha256:54c38050039232e1db4ad7375cfce6748d7b41c29e95a081c8a6d2c30364a2ce"}, {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"},
] ]
entrypoints = [ entrypoints = [
{file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"}, {file = "entrypoints-0.3-py2.py3-none-any.whl", hash = "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19"},
@ -987,6 +1037,9 @@ flake8-html = [
{file = "flake8-html-0.4.0.tar.gz", hash = "sha256:44bec37f142e97c4a5b2cf10efe24ed253617a9736878851a594d4763011e742"}, {file = "flake8-html-0.4.0.tar.gz", hash = "sha256:44bec37f142e97c4a5b2cf10efe24ed253617a9736878851a594d4763011e742"},
{file = "flake8_html-0.4.0-py2.py3-none-any.whl", hash = "sha256:f372cd599ba9a374943eaa75a9cce30408cf4c0cc2251bc5194e6b0d3fc2bc3a"}, {file = "flake8_html-0.4.0-py2.py3-none-any.whl", hash = "sha256:f372cd599ba9a374943eaa75a9cce30408cf4c0cc2251bc5194e6b0d3fc2bc3a"},
] ]
future = [
{file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"},
]
idna = [ idna = [
{file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"}, {file = "idna-2.9-py2.py3-none-any.whl", hash = "sha256:a068a21ceac8a4d63dbfd964670474107f541babbd2250d61922f029858365fa"},
{file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"}, {file = "idna-2.9.tar.gz", hash = "sha256:7588d1c14ae4c77d74036e8c22ff447b26d0fde8f007354fd48a7814db15b7cb"},
@ -996,8 +1049,8 @@ importlib-metadata = [
{file = "importlib_metadata-1.5.0.tar.gz", hash = "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302"}, {file = "importlib_metadata-1.5.0.tar.gz", hash = "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302"},
] ]
ipython = [ ipython = [
{file = "ipython-7.12.0-py3-none-any.whl", hash = "sha256:f6689108b1734501d3b59c84427259fd5ac5141afe2e846cfa8598eb811886c9"}, {file = "ipython-7.13.0-py3-none-any.whl", hash = "sha256:eb8d075de37f678424527b5ef6ea23f7b80240ca031c2dd6de5879d687a65333"},
{file = "ipython-7.12.0.tar.gz", hash = "sha256:d9459e7237e2e5858738ff9c3e26504b79899b58a6d49e574d352493d80684c6"}, {file = "ipython-7.13.0.tar.gz", hash = "sha256:ca478e52ae1f88da0102360e57e528b92f3ae4316aabac80a2cd7f7ab2efb48a"},
] ]
ipython-genutils = [ ipython-genutils = [
{file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"}, {file = "ipython_genutils-0.2.0-py2.py3-none-any.whl", hash = "sha256:72dd37233799e619666c9f639a9da83c34013a73e8bbc79a7a6348d93c61fab8"},
@ -1019,6 +1072,10 @@ livereload = [
{file = "livereload-2.6.1-py2.py3-none-any.whl", hash = "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b"}, {file = "livereload-2.6.1-py2.py3-none-any.whl", hash = "sha256:78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b"},
{file = "livereload-2.6.1.tar.gz", hash = "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"}, {file = "livereload-2.6.1.tar.gz", hash = "sha256:89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"},
] ]
lunr = [
{file = "lunr-0.5.6-py2.py3-none-any.whl", hash = "sha256:1208622930c915a07e6f8e8640474357826bad48534c0f57969b6fca9bffc88e"},
{file = "lunr-0.5.6.tar.gz", hash = "sha256:7be69d7186f65784a4f2adf81e5c58efd6a9921aa95966babcb1f2f2ada75c20"},
]
lxml = [ lxml = [
{file = "lxml-4.5.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c"}, {file = "lxml-4.5.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:0701f7965903a1c3f6f09328c1278ac0eee8f56f244e66af79cb224b7ef3801c"},
{file = "lxml-4.5.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd"}, {file = "lxml-4.5.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:06d4e0bbb1d62e38ae6118406d7cdb4693a3fa34ee3762238bcb96c9e36a93cd"},
@ -1083,6 +1140,11 @@ markupsafe = [
{file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"},
{file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"},
{file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"},
{file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"},
{file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"},
{file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"},
] ]
mccabe = [ mccabe = [
@ -1093,8 +1155,8 @@ mkautodoc = [
{file = "mkautodoc-0.1.0.tar.gz", hash = "sha256:7c2595f40276b356e576ce7e343338f8b4fa1e02ea904edf33fadf82b68ca67c"}, {file = "mkautodoc-0.1.0.tar.gz", hash = "sha256:7c2595f40276b356e576ce7e343338f8b4fa1e02ea904edf33fadf82b68ca67c"},
] ]
mkdocs = [ mkdocs = [
{file = "mkdocs-1.0.4-py2.py3-none-any.whl", hash = "sha256:8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"}, {file = "mkdocs-1.1-py2.py3-none-any.whl", hash = "sha256:1e385a70aea8a9dedb731aea4fd5f3704b2074801c4f96f06b2920999babda8a"},
{file = "mkdocs-1.0.4.tar.gz", hash = "sha256:17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939"}, {file = "mkdocs-1.1.tar.gz", hash = "sha256:9243291392f59e20b655e4e46210233453faf97787c2cf72176510e868143174"},
] ]
mkdocs-material = [ mkdocs-material = [
{file = "mkdocs-material-4.6.3.tar.gz", hash = "sha256:1d486635b03f5a2ec87325842f7b10c7ae7daa0eef76b185572eece6a6ea212c"}, {file = "mkdocs-material-4.6.3.tar.gz", hash = "sha256:1d486635b03f5a2ec87325842f7b10c7ae7daa0eef76b185572eece6a6ea212c"},
@ -1124,32 +1186,36 @@ multidict = [
{file = "multidict-4.7.5.tar.gz", hash = "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e"}, {file = "multidict-4.7.5.tar.gz", hash = "sha256:aee283c49601fa4c13adc64c09c978838a7e812f85377ae130a24d7198c0331e"},
] ]
mypy = [ mypy = [
{file = "mypy-0.761-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:7f672d02fffcbace4db2b05369142e0506cdcde20cea0e07c7c2171c4fd11dd6"}, {file = "mypy-0.770-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:a34b577cdf6313bf24755f7a0e3f3c326d5c1f4fe7422d1d06498eb25ad0c600"},
{file = "mypy-0.761-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:87c556fb85d709dacd4b4cb6167eecc5bbb4f0a9864b69136a0d4640fdc76a36"}, {file = "mypy-0.770-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:86c857510a9b7c3104cf4cde1568f4921762c8f9842e987bc03ed4f160925754"},
{file = "mypy-0.761-cp35-cp35m-win_amd64.whl", hash = "sha256:c6d27bd20c3ba60d5b02f20bd28e20091d6286a699174dfad515636cb09b5a72"}, {file = "mypy-0.770-cp35-cp35m-win_amd64.whl", hash = "sha256:a8ffcd53cb5dfc131850851cc09f1c44689c2812d0beb954d8138d4f5fc17f65"},
{file = "mypy-0.761-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:4b9365ade157794cef9685791032521233729cb00ce76b0ddc78749abea463d2"}, {file = "mypy-0.770-cp36-cp36m-macosx_10_6_x86_64.whl", hash = "sha256:7687f6455ec3ed7649d1ae574136835a4272b65b3ddcf01ab8704ac65616c5ce"},
{file = "mypy-0.761-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:634aef60b4ff0f650d3e59d4374626ca6153fcaff96ec075b215b568e6ee3cb0"}, {file = "mypy-0.770-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:3beff56b453b6ef94ecb2996bea101a08f1f8a9771d3cbf4988a61e4d9973761"},
{file = "mypy-0.761-cp36-cp36m-win_amd64.whl", hash = "sha256:53ea810ae3f83f9c9b452582261ea859828a9ed666f2e1ca840300b69322c474"}, {file = "mypy-0.770-cp36-cp36m-win_amd64.whl", hash = "sha256:15b948e1302682e3682f11f50208b726a246ab4e6c1b39f9264a8796bb416aa2"},
{file = "mypy-0.761-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a"}, {file = "mypy-0.770-cp37-cp37m-macosx_10_6_x86_64.whl", hash = "sha256:b90928f2d9eb2f33162405f32dde9f6dcead63a0971ca8a1b50eb4ca3e35ceb8"},
{file = "mypy-0.761-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:7eadc91af8270455e0d73565b8964da1642fe226665dd5c9560067cd64d56749"}, {file = "mypy-0.770-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c56ffe22faa2e51054c5f7a3bc70a370939c2ed4de308c690e7949230c995913"},
{file = "mypy-0.761-cp37-cp37m-win_amd64.whl", hash = "sha256:e2bb577d10d09a2d8822a042a23b8d62bc3b269667c9eb8e60a6edfa000211b1"}, {file = "mypy-0.770-cp37-cp37m-win_amd64.whl", hash = "sha256:8dfb69fbf9f3aeed18afffb15e319ca7f8da9642336348ddd6cab2713ddcf8f9"},
{file = "mypy-0.761-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2c35cae79ceb20d47facfad51f952df16c2ae9f45db6cb38405a3da1cf8fc0a7"}, {file = "mypy-0.770-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:219a3116ecd015f8dca7b5d2c366c973509dfb9a8fc97ef044a36e3da66144a1"},
{file = "mypy-0.761-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:f97a605d7c8bc2c6d1172c2f0d5a65b24142e11a58de689046e62c2d632ca8c1"}, {file = "mypy-0.770-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7ec45a70d40ede1ec7ad7f95b3c94c9cf4c186a32f6bacb1795b60abd2f9ef27"},
{file = "mypy-0.761-cp38-cp38-win_amd64.whl", hash = "sha256:a6bd44efee4dc8c3324c13785a9dc3519b3ee3a92cada42d2b57762b7053b49b"}, {file = "mypy-0.770-cp38-cp38-win_amd64.whl", hash = "sha256:f91c7ae919bbc3f96cd5e5b2e786b2b108343d1d7972ea130f7de27fdd547cf3"},
{file = "mypy-0.761-py3-none-any.whl", hash = "sha256:7e396ce53cacd5596ff6d191b47ab0ea18f8e0ec04e15d69728d530e86d4c217"}, {file = "mypy-0.770-py3-none-any.whl", hash = "sha256:3b1fc683fb204c6b4403a1ef23f0b1fac8e4477091585e0c8c54cbdf7d7bb164"},
{file = "mypy-0.761.tar.gz", hash = "sha256:85baab8d74ec601e86134afe2bcccd87820f79d2f8d5798c889507d1088287bf"}, {file = "mypy-0.770.tar.gz", hash = "sha256:8a627507ef9b307b46a1fea9513d5c98680ba09591253082b4c48697ba05a4ae"},
] ]
mypy-extensions = [ mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
] ]
nltk = [
{file = "nltk-3.4.5.win32.exe", hash = "sha256:a08bdb4b8a1c13de16743068d9eb61c8c71c2e5d642e8e08205c528035843f82"},
{file = "nltk-3.4.5.zip", hash = "sha256:bed45551259aa2101381bbdd5df37d44ca2669c5c3dad72439fa459b29137d94"},
]
packaging = [ packaging = [
{file = "packaging-20.1-py2.py3-none-any.whl", hash = "sha256:170748228214b70b672c581a3dd610ee51f733018650740e98c7df862a583f73"}, {file = "packaging-20.3-py2.py3-none-any.whl", hash = "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"},
{file = "packaging-20.1.tar.gz", hash = "sha256:e665345f9eef0c621aa0bf2f8d78cf6d21904eef16a93f020240b704a57f1334"}, {file = "packaging-20.3.tar.gz", hash = "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3"},
] ]
parso = [ parso = [
{file = "parso-0.6.1-py2.py3-none-any.whl", hash = "sha256:951af01f61e6dccd04159042a0706a31ad437864ec6e25d0d7a96a9fbb9b0095"}, {file = "parso-0.6.2-py2.py3-none-any.whl", hash = "sha256:8515fc12cfca6ee3aa59138741fc5624d62340c97e401c74875769948d4f2995"},
{file = "parso-0.6.1.tar.gz", hash = "sha256:56b2105a80e9c4df49de85e125feb6be69f49920e121406f15e7acde6c9dfc57"}, {file = "parso-0.6.2.tar.gz", hash = "sha256:0c5659e0c6eba20636f99a04f469798dca8da279645ce5c387315b2c23912157"},
] ]
pathspec = [ pathspec = [
{file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"}, {file = "pathspec-0.7.0-py2.py3-none-any.whl", hash = "sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424"},
@ -1168,8 +1234,8 @@ pluggy = [
{file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"},
] ]
prompt-toolkit = [ prompt-toolkit = [
{file = "prompt_toolkit-3.0.3-py3-none-any.whl", hash = "sha256:c93e53af97f630f12f5f62a3274e79527936ed466f038953dfa379d4941f651a"}, {file = "prompt_toolkit-3.0.4-py3-none-any.whl", hash = "sha256:859e1b205b6cf6a51fa57fa34202e45365cf58f8338f0ee9f4e84a4165b37d5b"},
{file = "prompt_toolkit-3.0.3.tar.gz", hash = "sha256:a402e9bf468b63314e37460b68ba68243d55b2f8c4d0192f85a019af3945050e"}, {file = "prompt_toolkit-3.0.4.tar.gz", hash = "sha256:ebe6b1b08c888b84c50d7f93dee21a09af39860144ff6130aadbd61ae8d29783"},
] ]
ptyprocess = [ ptyprocess = [
{file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"}, {file = "ptyprocess-0.6.0-py2.py3-none-any.whl", hash = "sha256:d7cc528d76e76342423ca640335bd3633420dc1366f258cb31d05e865ef5ca1f"},
@ -1204,8 +1270,8 @@ pyflakes = [
{file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"}, {file = "pyflakes-2.1.1.tar.gz", hash = "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"},
] ]
pygments = [ pygments = [
{file = "Pygments-2.5.2-py2.py3-none-any.whl", hash = "sha256:2a3fe295e54a20164a9df49c75fa58526d3be48e14aceba6d6b1e8ac0bfd6f1b"}, {file = "Pygments-2.6.1-py3-none-any.whl", hash = "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"},
{file = "Pygments-2.5.2.tar.gz", hash = "sha256:98c8aa5a9f778fcd1026a17361ddaf7330d1b7c62ae97c3bb0ae73e0b9b6b0fe"}, {file = "Pygments-2.6.1.tar.gz", hash = "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44"},
] ]
pymdown-extensions = [ pymdown-extensions = [
{file = "pymdown-extensions-6.3.tar.gz", hash = "sha256:cb879686a586b22292899771f5e5bc3382808e92aa938f71b550ecdea709419f"}, {file = "pymdown-extensions-6.3.tar.gz", hash = "sha256:cb879686a586b22292899771f5e5bc3382808e92aa938f71b550ecdea709419f"},
@ -1216,8 +1282,8 @@ pyparsing = [
{file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"}, {file = "pyparsing-2.4.6.tar.gz", hash = "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f"},
] ]
pytest = [ pytest = [
{file = "pytest-5.3.5-py3-none-any.whl", hash = "sha256:ff615c761e25eb25df19edddc0b970302d2a9091fbce0e7213298d85fb61fef6"}, {file = "pytest-5.4.1-py3-none-any.whl", hash = "sha256:0e5b30f5cb04e887b91b1ee519fa3d89049595f428c1db76e73bd7f17b09b172"},
{file = "pytest-5.3.5.tar.gz", hash = "sha256:0d5fe9189a148acc3c3eb2ac8e1ac0742cb7618c084f3d228baaec0c254b318d"}, {file = "pytest-5.4.1.tar.gz", hash = "sha256:84dde37075b8805f3d1f392cc47e38a0e59518fb46a431cfdaf7cf1ce805f970"},
] ]
pytest-asyncio = [ pytest-asyncio = [
{file = "pytest-asyncio-0.10.0.tar.gz", hash = "sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf"}, {file = "pytest-asyncio-0.10.0.tar.gz", hash = "sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf"},
@ -1228,8 +1294,8 @@ pytest-cov = [
{file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"}, {file = "pytest_cov-2.8.1-py2.py3-none-any.whl", hash = "sha256:cdbdef4f870408ebdbfeb44e63e07eb18bb4619fae852f6e760645fa36172626"},
] ]
pytest-html = [ pytest-html = [
{file = "pytest-html-2.0.1.tar.gz", hash = "sha256:933da7a5e71e5eace9e475441ed88a684f20f6198aa36516cb947ac05edd9921"}, {file = "pytest-html-2.1.0.tar.gz", hash = "sha256:8645a8616c8ed7414678e0aeebc3b2fd7d44268773ef5e7289289ad8632c9e91"},
{file = "pytest_html-2.0.1-py2.py3-none-any.whl", hash = "sha256:bc40553ca2a1835479c2caf7d48604502cd66d0c5db58ddbca53d74946ee71bd"}, {file = "pytest_html-2.1.0-py2.py3-none-any.whl", hash = "sha256:0317a0a589db59c26091ab6068b3edac8d9bc1a8bb9727ade48f806797346956"},
] ]
pytest-metadata = [ pytest-metadata = [
{file = "pytest-metadata-1.8.0.tar.gz", hash = "sha256:2071a59285de40d7541fde1eb9f1ddea1c9db165882df82781367471238b66ba"}, {file = "pytest-metadata-1.8.0.tar.gz", hash = "sha256:2071a59285de40d7541fde1eb9f1ddea1c9db165882df82781367471238b66ba"},
@ -1293,13 +1359,15 @@ toml = [
{file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"}, {file = "toml-0.10.0.tar.gz", hash = "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c"},
] ]
tornado = [ tornado = [
{file = "tornado-6.0.3-cp35-cp35m-win32.whl", hash = "sha256:c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"}, {file = "tornado-6.0.4-cp35-cp35m-win32.whl", hash = "sha256:5217e601700f24e966ddab689f90b7ea4bd91ff3357c3600fa1045e26d68e55d"},
{file = "tornado-6.0.3-cp35-cp35m-win_amd64.whl", hash = "sha256:398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60"}, {file = "tornado-6.0.4-cp35-cp35m-win_amd64.whl", hash = "sha256:c98232a3ac391f5faea6821b53db8db461157baa788f5d6222a193e9456e1740"},
{file = "tornado-6.0.3-cp36-cp36m-win32.whl", hash = "sha256:4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281"}, {file = "tornado-6.0.4-cp36-cp36m-win32.whl", hash = "sha256:5f6a07e62e799be5d2330e68d808c8ac41d4a259b9cea61da4101b83cb5dc673"},
{file = "tornado-6.0.3-cp36-cp36m-win_amd64.whl", hash = "sha256:349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c"}, {file = "tornado-6.0.4-cp36-cp36m-win_amd64.whl", hash = "sha256:c952975c8ba74f546ae6de2e226ab3cc3cc11ae47baf607459a6728585bb542a"},
{file = "tornado-6.0.3-cp37-cp37m-win32.whl", hash = "sha256:559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5"}, {file = "tornado-6.0.4-cp37-cp37m-win32.whl", hash = "sha256:2c027eb2a393d964b22b5c154d1a23a5f8727db6fda837118a776b29e2b8ebc6"},
{file = "tornado-6.0.3-cp37-cp37m-win_amd64.whl", hash = "sha256:abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7"}, {file = "tornado-6.0.4-cp37-cp37m-win_amd64.whl", hash = "sha256:5618f72e947533832cbc3dec54e1dffc1747a5cb17d1fd91577ed14fa0dc081b"},
{file = "tornado-6.0.3.tar.gz", hash = "sha256:c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9"}, {file = "tornado-6.0.4-cp38-cp38-win32.whl", hash = "sha256:22aed82c2ea340c3771e3babc5ef220272f6fd06b5108a53b4976d0d722bcd52"},
{file = "tornado-6.0.4-cp38-cp38-win_amd64.whl", hash = "sha256:c58d56003daf1b616336781b26d184023ea4af13ae143d9dda65e31e534940b9"},
{file = "tornado-6.0.4.tar.gz", hash = "sha256:0fe2d45ba43b00a41cd73f8be321a44936dc1aba233dee979f17a042b83eb6dc"},
] ]
traitlets = [ traitlets = [
{file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"}, {file = "traitlets-4.3.3-py2.py3-none-any.whl", hash = "sha256:70b4c6a1d9019d7b4f6846832288f86998aa3b9207c6821f3578a6a6a467fe44"},
@ -1368,6 +1436,6 @@ yarl = [
{file = "yarl-1.4.2.tar.gz", hash = "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b"}, {file = "yarl-1.4.2.tar.gz", hash = "sha256:58cd9c469eced558cd81aa3f484b2924e8897049e06889e8ff2510435b7ef74b"},
] ]
zipp = [ zipp = [
{file = "zipp-3.0.0-py3-none-any.whl", hash = "sha256:12248a63bbdf7548f89cb4c7cda4681e537031eda29c02ea29674bc6854460c2"}, {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"},
{file = "zipp-3.0.0.tar.gz", hash = "sha256:7c0f8e91abc0dc07a5068f315c52cb30c66bfbc581e5b50704c8a2f6ebae794a"}, {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"},
] ]

View file

@ -53,7 +53,7 @@ black = {version = "^19.0", allow-prereleases = true}
isort = "^4.3" isort = "^4.3"
flake8 = "^3.7" flake8 = "^3.7"
flake8-html = "^0.4.0" flake8-html = "^0.4.0"
mypy = "^0.761" mypy = "^0.770"
mkdocs = "^1.0" mkdocs = "^1.0"
mkdocs-material = "^4.6" mkdocs-material = "^4.6"
mkautodoc = "^0.1.0" mkautodoc = "^0.1.0"

View file

@ -1,6 +1,6 @@
import pytest import pytest
from aiogram.api.client.base import BaseBot from aiogram import Bot
from aiogram.api.client.session.aiohttp import AiohttpSession from aiogram.api.client.session.aiohttp import AiohttpSession
from aiogram.api.methods import GetMe from aiogram.api.methods import GetMe
@ -12,22 +12,22 @@ except ImportError:
class TestBaseBot: class TestBaseBot:
def test_init(self): def test_init(self):
base_bot = BaseBot("42:TEST") base_bot = Bot("42:TEST")
assert isinstance(base_bot.session, AiohttpSession) assert isinstance(base_bot.session, AiohttpSession)
assert base_bot.id == 42 assert base_bot.id == 42
def test_hashable(self): def test_hashable(self):
base_bot = BaseBot("42:TEST") base_bot = Bot("42:TEST")
assert hash(base_bot) == hash("42:TEST") assert hash(base_bot) == hash("42:TEST")
def test_equals(self): def test_equals(self):
base_bot = BaseBot("42:TEST") base_bot = Bot("42:TEST")
assert base_bot == BaseBot("42:TEST") assert base_bot == Bot("42:TEST")
assert base_bot != "42:TEST" assert base_bot != "42:TEST"
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_emit(self): async def test_emit(self):
base_bot = BaseBot("42:TEST") base_bot = Bot("42:TEST")
method = GetMe() method = GetMe()
@ -40,7 +40,7 @@ class TestBaseBot:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_close(self): async def test_close(self):
base_bot = BaseBot("42:TEST", session=AiohttpSession()) base_bot = Bot("42:TEST", session=AiohttpSession())
await base_bot.session.create_session() await base_bot.session.create_session()
with patch( with patch(
@ -55,10 +55,8 @@ class TestBaseBot:
with patch( with patch(
"aiogram.api.client.session.aiohttp.AiohttpSession.close", new_callable=CoroutineMock "aiogram.api.client.session.aiohttp.AiohttpSession.close", new_callable=CoroutineMock
) as mocked_close: ) as mocked_close:
async with BaseBot("42:TEST", session=AiohttpSession()).context( async with Bot("42:TEST", session=AiohttpSession()).context(auto_close=close) as bot:
auto_close=close assert isinstance(bot, Bot)
) as bot:
assert isinstance(bot, BaseBot)
if close: if close:
mocked_close.assert_awaited() mocked_close.assert_awaited()
else: else:

View file

@ -6,7 +6,6 @@ import pytest
from aiogram.api.client.session.base import BaseSession, T from aiogram.api.client.session.base import BaseSession, T
from aiogram.api.client.telegram import PRODUCTION, TelegramAPIServer from aiogram.api.client.telegram import PRODUCTION, TelegramAPIServer
from aiogram.api.methods import GetMe, Response, TelegramMethod from aiogram.api.methods import GetMe, Response, TelegramMethod
from aiogram.utils.mixins import DataMixin
try: try:
from asynctest import CoroutineMock, patch from asynctest import CoroutineMock, patch
@ -31,7 +30,7 @@ class CustomSession(BaseSession):
yield b"\f" * 10 yield b"\f" * 10
class TestBaseSession(DataMixin): class TestBaseSession:
def test_init_api(self): def test_init_api(self):
session = CustomSession() session = CustomSession()
assert session.api == PRODUCTION assert session.api == PRODUCTION

View file

@ -23,7 +23,6 @@ class TestBaseClassBasedHandler:
assert handler.event == event assert handler.event == event
assert handler.data["key"] == 42 assert handler.data["key"] == 42
assert hasattr(handler, "bot")
assert not hasattr(handler, "filters") assert not hasattr(handler, "filters")
assert await handler == 42 assert await handler == 42
@ -33,7 +32,8 @@ class TestBaseClassBasedHandler:
handler = MyHandler(event=event, key=42) handler = MyHandler(event=event, key=42)
bot = Bot("42:TEST") bot = Bot("42:TEST")
assert handler.bot is None with pytest.raises(LookupError):
handler.bot
Bot.set_current(bot) Bot.set_current(bot)
assert handler.bot == bot assert handler.bot == bot

View file

@ -40,6 +40,11 @@ class TestHelper:
class MyHelper(Helper): class MyHelper(Helper):
kaboom = Item() kaboom = Item()
def test_not_a_helper_subclass(self):
with pytest.raises(RuntimeError):
class NotAHelperSubclass:
A = Item()
class TestHelperMode: class TestHelperMode:
def test_helper_mode_all(self): def test_helper_mode_all(self):

View file

@ -1,13 +1,16 @@
import pytest import pytest
from aiogram.utils.mixins import ContextInstanceMixin, DataMixin from aiogram.utils.mixins import (
ContextInstanceMixin,
DataMixin,
)
class DataObject(DataMixin): class ContextObject(ContextInstanceMixin["ContextObject"]):
pass pass
class ContextObject(ContextInstanceMixin): class DataObject(DataMixin):
pass pass