diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 304814a..688a542 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -29,7 +29,7 @@ jobs: with: args: tox - build: + tests: needs: lint runs-on: ubuntu-latest @@ -38,11 +38,10 @@ jobs: matrix: toxenv: - - py35 - - py36 - py37 - py38 - py39 + - py310 steps: - uses: actions/checkout@v2 @@ -50,7 +49,9 @@ jobs: - name: tox ${{ matrix.toxenv }} uses: docker://snakepacker/python:all env: - TOXENV: ${{ matrix.toxenv }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + FORCE_COLOR: yes + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TOXENV: ${{ matrix.toxenv }} with: args: tox diff --git a/pytest.ini b/pytest.ini index 7d36762..2d2f17d 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,3 +2,4 @@ python_files = test_* python_functions = test_* python_classes = TestSuite* +asyncio_mode=auto diff --git a/setup.py b/setup.py index 028ad45..aa8e456 100644 --- a/setup.py +++ b/setup.py @@ -10,6 +10,16 @@ ).load_module() +TEST_REQUIRES = [ + "async-timeout~=4.0.2", + "pytest~=7.1.2", + "pytest-aiohttp~=1.0.4", + "pytest-cov~=3.0.0", + "coverage!=4.3", + "coveralls~=3.3.1", +] + + setup( name="wsrpc-aiohttp", version=module.__version__, @@ -31,11 +41,10 @@ "Operating System :: Microsoft", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", ], long_description=open("README.md").read(), @@ -46,24 +55,11 @@ python_requires=">3.5.*, <4", extras_require={ "ujson": ["ujson"], - "testing": [ - "async-timeout", - "pytest", - "pytest-aiohttp", - "pytest-cov", - "coverage!=4.3", - "coveralls", - ], + "testing": TEST_REQUIRES, "develop": [ - "async-timeout", - "coverage!=4.3", - "coveralls", - "pytest", - "pytest-aiohttp", - "pytest-cov", "Sphinx", "sphinxcontrib-plantuml", "tox>=2.4", - ], + ] + TEST_REQUIRES, }, ) diff --git a/tests/conftest.py b/tests/conftest.py index f3dfcb1..a88e04e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,10 +22,12 @@ def application(handler, socket_path): @pytest.fixture -async def session(aiohttp_client, application, loop) -> ClientSession: +async def session(aiohttp_client, application, event_loop) -> ClientSession: return await aiohttp_client(application) @pytest.fixture -async def client(session: ClientSession, socket_path, loop) -> WSRPCClient: - return WSRPCClient(socket_path, session=session, loop=loop) +async def client( + session: ClientSession, socket_path, event_loop +) -> WSRPCClient: + return WSRPCClient(socket_path, session=session, loop=event_loop) diff --git a/tests/test_events.py b/tests/test_events.py index 10c4198..912311c 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -5,11 +5,11 @@ async def emitter(socket: WSRPCBase): await socket.emit({"Hello": "world"}) -async def test_emitter(client: WSRPCClient, handler, loop): +async def test_emitter(client: WSRPCClient, handler, event_loop): handler.add_route("emitter", emitter) async with client: - future = loop.create_future() + future = event_loop.create_future() client.add_event_listener(future.set_result) diff --git a/tests/test_route.py b/tests/test_route.py index 2e8c26e..e9b3dfc 100644 --- a/tests/test_route.py +++ b/tests/test_route.py @@ -102,23 +102,21 @@ def foobar(self): assert "foobar" not in FooRoute.__no_proxy__ -def test_route__init__(loop): +async def test_route__init__(event_loop): socket = Mock() - socket._loop = object() - route = Route(socket) assert route.socket is socket - assert route.loop is socket._loop + assert route.loop is event_loop socket = object() route = Route(socket) assert route.socket is socket - assert route.loop is loop + assert route.loop is event_loop -def test_abc_inheritance(loop): +def test_abc_inheritance(event_loop): class AbstractMixin(ABC): @abstractmethod def foo(self): diff --git a/tests/test_rpc_route.py b/tests/test_rpc_route.py index aeb4666..1bbc6f2 100644 --- a/tests/test_rpc_route.py +++ b/tests/test_rpc_route.py @@ -151,10 +151,10 @@ async def test_call_when_params_none( async def test_broadcast( - client: WSRPCClient, handler: WebSocketAsync, route: Route, loop + client: WSRPCClient, handler: WebSocketAsync, route: Route, event_loop ): async with client: - future = loop.create_future() + future = event_loop.create_future() async def on_broadcast(_, result): nonlocal future @@ -229,7 +229,7 @@ def get_data(self, _): async def test_call_timeout(client: WSRPCClient, handler: WebSocketAsync): async def will_sleep_for(_, seconds): - with timeout(0.5): + async with timeout(0.5): await asyncio.sleep(seconds) return DATA_TO_RETURN diff --git a/tox.ini b/tox.ini index 80fcca5..8f2997d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,14 @@ [tox] -envlist = lint,mypy,py3{5,6,7,8,9} +envlist = lint,mypy,py3{7,8,9,10} [testenv] -passenv = COVERALLS_* AMQP_* +passenv = COVERALLS_* AMQP_* GITHUB_* FORCE_COLOR extras = testing commands= - py.test -vv -p no:asyncio \ + py.test -vv \ --cov=wsrpc_aiohttp \ --cov-report=term-missing \ --doctest-modules \ @@ -21,7 +21,7 @@ deps = pylava commands= - pylava -o pylava.ini . + pylava -o pylava.ini wsrpc_aiohttp tests [testenv:mypy] usedevelop = true diff --git a/wsrpc_aiohttp/version.py b/wsrpc_aiohttp/version.py index 4d5c452..2e301e6 100644 --- a/wsrpc_aiohttp/version.py +++ b/wsrpc_aiohttp/version.py @@ -5,7 +5,7 @@ team_email = "me@mosquito.su" -version_info = (3, 1, 2) +version_info = (3, 2, 1) __author__ = ", ".join("{} <{}>".format(*info) for info in author_info) diff --git a/wsrpc_aiohttp/websocket/abc.py b/wsrpc_aiohttp/websocket/abc.py index f30605e..cb9a806 100644 --- a/wsrpc_aiohttp/websocket/abc.py +++ b/wsrpc_aiohttp/websocket/abc.py @@ -1,8 +1,5 @@ import asyncio -from abc import ( - ABC, abstractmethod, abstractclassmethod, abstractproperty, - abstractstaticmethod -) +from abc import ABC, abstractmethod from enum import IntEnum from typing import Any, Mapping, Coroutine, Union, Callable, Dict, Tuple @@ -16,7 +13,8 @@ class AbstractWebSocket(ABC): def __init__(self, request: Request): raise NotImplementedError(request) - @abstractclassmethod + @classmethod + @abstractmethod def configure(cls, keepalive_timeout: int, client_timeout: int, max_concurrent_requests: int) -> None: @@ -55,7 +53,8 @@ async def authorize(self) -> bool: async def __handle_request(self) -> WebSocketResponse: raise NotImplementedError - @abstractclassmethod + @classmethod + @abstractmethod def broadcast( cls, func, callback=None, return_exceptions=True, **kwargs: Mapping[str, Any] @@ -115,7 +114,7 @@ def __getattr__(self, item: str): EventListenerType = Callable[[Dict[str, Any]], Any] -class AbstactWSRPC(ABC): +class WSRPCBase(ABC): @abstractmethod def __init__(self, loop: asyncio.AbstractEventLoop = None, timeout: Union[int, float] = None): @@ -138,20 +137,22 @@ async def handle_message(self, message: WSMessage): async def _on_message(self, msg: WSMessage): raise NotImplementedError - @abstractclassmethod + @classmethod + @abstractmethod def get_routes(cls) -> Mapping[str, "RouteType"]: raise NotImplementedError @classmethod - def get_clients(cls) -> Dict[str, "AbstactWSRPC"]: + def get_clients(cls) -> Dict[str, "AbstractWSRPC"]: raise NotImplementedError - @abstractproperty + @property + @abstractmethod def routes(self) -> Dict[str, "RouteType"]: raise NotImplementedError @property - def clients(self) -> Dict[str, "AbstactWSRPC"]: + def clients(self) -> Dict[str, "AbstractWSRPC"]: """ Property which contains the socket clients """ raise NotImplementedError @@ -159,7 +160,8 @@ def clients(self) -> Dict[str, "AbstactWSRPC"]: def prepare_args(self, args) -> Tuple[Tuple[Any, ...], Dict[str, Any]]: raise NotImplementedError - @abstractstaticmethod + @staticmethod + @abstractmethod def is_route(func) -> bool: raise NotImplementedError @@ -217,30 +219,6 @@ async def make_something(self, foo, bar): async def emit(self, event: Any) -> None: pass - @abstractclassmethod - def add_route(cls, route: str, - handler: Union[AbstractRoute, Callable]) -> None: - """ Expose local function through RPC - - :param route: Name which function will be aliased for this function. - Remote side should call function by this name. - :param handler: Function or Route class (classes based on - :class:`wsrpc_aiohttp.WebSocketRoute`). - For route classes the public methods will - be registered automatically. - - .. note:: - - Route classes might be initialized only once for the each - socket instance. - - In case the method of class will be called first, - :func:`wsrpc_aiohttp.WebSocketRoute.init` will be called - without params before callable method. - - """ - raise NotImplementedError - @abstractmethod def add_event_listener(self, func: EventListenerType) -> None: raise NotImplementedError @@ -255,7 +233,8 @@ def remove_route(cls, route: str, fail=True): raise NotImplementedError - @abstractproperty + @property + @abstractmethod def proxy(self) -> Proxy: """ Special property which allow run the remote functions by `dot` notation @@ -272,8 +251,39 @@ def proxy(self) -> Proxy: RouteType = Union[ - Callable[[AbstactWSRPC, Any], Any], - Callable[[AbstactWSRPC, Any], Coroutine[Any, None, Any]], + Callable[[WSRPCBase, Any], Any], + Callable[[WSRPCBase, Any], Coroutine[Any, None, Any]], AbstractRoute ] + + +class AbstractWSRPC(WSRPCBase, ABC): + @classmethod + @abstractmethod + def add_route(cls, route: str, handler: RouteType) -> None: + """ Expose local function through RPC + + :param route: Name which function will be aliased for this function. + Remote side should call function by this name. + :param handler: Function or Route class (classes based on + :class:`wsrpc_aiohttp.WebSocketRoute`). + For route classes the public methods will + be registered automatically. + + .. note:: + + Route classes might be initialized only once for each + socket instance. + + In case the method of class will be called first, + :func:`wsrpc_aiohttp.WebSocketRoute.init` will be called + without params before callable method. + + """ + raise NotImplementedError + + +# backward compatibility for typo +# noinspection SpellCheckingInspection +AbstactWSRPC = AbstractWSRPC FrameMappingItemType = Mapping[IntEnum, Callable[[WSMessage], Any]] diff --git a/wsrpc_aiohttp/websocket/common.py b/wsrpc_aiohttp/websocket/common.py index 473d20d..8acb178 100644 --- a/wsrpc_aiohttp/websocket/common.py +++ b/wsrpc_aiohttp/websocket/common.py @@ -10,7 +10,7 @@ from . import decorators from .abc import ( - Proxy, AbstactWSRPC, FrameMappingItemType, RouteType, EventListenerType + Proxy, AbstractWSRPC, FrameMappingItemType, RouteType, EventListenerType ) from .route import Route from .tools import Singleton, awaitable, loads @@ -59,10 +59,10 @@ def __repr__(self): RouteCollectionType = t.DefaultDict[ - t.Type[AbstactWSRPC], t.Dict[str, RouteType] + t.Type[AbstractWSRPC], t.Dict[str, RouteType] ] ClientCollectionType = t.DefaultDict[ - t.Type[AbstactWSRPC], t.Dict[str, AbstactWSRPC] + t.Type[AbstractWSRPC], t.Dict[str, AbstractWSRPC] ] LocksCollectionType = t.DefaultDict[int, asyncio.Lock] FutureCollectionType = t.DefaultDict[int, asyncio.Future] @@ -73,7 +73,7 @@ def _route_maker() -> t.Dict[str, RouteType]: return {"ping": ping} # type: ignore -class WSRPCBase(AbstactWSRPC): +class WSRPCBase(AbstractWSRPC): """ Common WSRPC abstraction """ _ROUTES = defaultdict(_route_maker) # type: RouteCollectionType @@ -249,7 +249,7 @@ def get_routes(cls) -> t.Dict[str, RouteType]: return cls._ROUTES[cls] @classmethod - def get_clients(cls) -> t.Dict[str, AbstactWSRPC]: + def get_clients(cls) -> t.Dict[str, AbstractWSRPC]: return cls._CLIENTS[cls] @property @@ -258,7 +258,7 @@ def routes(self) -> t.Dict[str, RouteType]: return self.get_routes() @property - def clients(self) -> t.Dict[str, AbstactWSRPC]: + def clients(self) -> t.Dict[str, AbstractWSRPC]: """ Property which contains the socket clients """ return self.get_clients() @@ -451,7 +451,7 @@ async def emit(self, event): await self._send(**event) @classmethod - def add_route(cls, route: str, handler: RouteType): + def add_route(cls, route: str, handler: RouteType) -> None: """ Expose local function through RPC :param route: Name which function will be aliased for this function. diff --git a/wsrpc_aiohttp/websocket/route.py b/wsrpc_aiohttp/websocket/route.py index 392787e..d3b42b6 100644 --- a/wsrpc_aiohttp/websocket/route.py +++ b/wsrpc_aiohttp/websocket/route.py @@ -2,7 +2,7 @@ import logging from abc import ABCMeta from types import MappingProxyType -from typing import Any, Callable, Mapping +from typing import Any, Callable, Mapping, Optional from . import decorators from .abc import AbstractRoute, AbstractWebSocket @@ -63,10 +63,7 @@ class RouteBase(AbstractRoute, metaclass=RouteMeta): def __init__(self, socket: AbstractWebSocket): super().__init__(socket) self.__socket = socket - self.__loop = getattr(self.socket, "_loop", None) - - if self.__loop is None: - self.__loop = asyncio.get_event_loop() + self.__loop: Optional[asyncio.AbstractEventLoop] = None @property def socket(self) -> AbstractWebSocket: @@ -74,6 +71,8 @@ def socket(self) -> AbstractWebSocket: @property def loop(self) -> asyncio.AbstractEventLoop: + if self.__loop is None: + self.__loop = asyncio.get_running_loop() return self.__loop def _onclose(self):