-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improvements to IPConnection #26
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,41 @@ | ||
import asyncio | ||
import codecs | ||
from dataclasses import dataclass | ||
from typing import Any, Callable, Coroutine, cast | ||
|
||
_AsyncFuncType = Callable[..., Coroutine[Any, Any, Any]] | ||
|
||
|
||
def _with_lock(func: _AsyncFuncType) -> _AsyncFuncType: | ||
async def with_lock(*args: Any, **kwargs: Any) -> None: | ||
self = args[0] | ||
async with self.lock: | ||
await func(*args, **kwargs) | ||
|
||
return cast(_AsyncFuncType, with_lock) | ||
|
||
|
||
def _ensure_connected(func: _AsyncFuncType) -> _AsyncFuncType: | ||
""" | ||
Decorator function to check if the wrapper is connected to the device | ||
before calling the attached function. | ||
|
||
Args: | ||
func: Function to call if connected to device | ||
|
||
Returns: | ||
The wrapped function. | ||
|
||
""" | ||
|
||
async def check_connected(*args: Any, **kwargs: Any) -> None: | ||
self = args[0] | ||
if self._reader is None or self._writer is None: | ||
raise DisconnectedError("Need to call connect() before using IPConnection.") | ||
else: | ||
await func(*args, **kwargs) | ||
|
||
return cast(_AsyncFuncType, check_connected) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case I would prefer to make a wrapper around I feel like this shouldn't be necessary. I expect am misunderstanding something here, but the asyncio API for this seems clunky. Thoughts? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I did realise this probably does the same thing as SerialConnection does in #24, except that uses the |
||
|
||
|
||
class DisconnectedError(Exception): | ||
|
@@ -16,39 +52,43 @@ | |
def __init__(self): | ||
self._reader, self._writer = (None, None) | ||
self._lock = asyncio.Lock() | ||
self.connected: bool = False | ||
|
||
@property | ||
def lock(self) -> asyncio.Lock: | ||
return self._lock | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be removed if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Originally I didn't have this property, I only created it as the linter complained. The intention is that this would only be used in the decorator so wrap the function in the context manager, not use outside the class. |
||
|
||
async def connect(self, settings: IPConnectionSettings): | ||
self._reader, self._writer = await asyncio.open_connection( | ||
settings.ip, settings.port | ||
) | ||
|
||
def ensure_connected(self): | ||
if self._reader is None or self._writer is None: | ||
raise DisconnectedError("Need to call connect() before using IPConnection.") | ||
|
||
async def send_command(self, message) -> None: | ||
async with self._lock: | ||
self.ensure_connected() | ||
await self._send_message(message) | ||
@_with_lock | ||
@_ensure_connected | ||
async def send_command(self, message: str) -> None: | ||
await self._send_message(message) | ||
|
||
async def send_query(self, message) -> str: | ||
async with self._lock: | ||
self.ensure_connected() | ||
await self._send_message(message) | ||
return await self._receive_response() | ||
@_with_lock | ||
@_ensure_connected | ||
async def send_query(self, message: str) -> str: | ||
await self._send_message(message) | ||
return await self._receive_response() | ||
|
||
# TODO: Figure out type hinting for connections. TypeGuard fails to work as expected | ||
@_with_lock | ||
@_ensure_connected | ||
async def close(self): | ||
async with self._lock: | ||
self.ensure_connected() | ||
self._writer.close() | ||
await self._writer.wait_closed() | ||
self._reader, self._writer = (None, None) | ||
|
||
async def _send_message(self, message) -> None: | ||
self._writer.write(message.encode("utf-8")) | ||
assert isinstance(self._writer, asyncio.StreamWriter) | ||
self._writer.close() | ||
await self._writer.wait_closed() | ||
self._reader, self._writer = (None, None) | ||
|
||
async def _send_message(self, message: str) -> None: | ||
assert isinstance(self._writer, asyncio.StreamWriter) | ||
self._writer.write(codecs.encode(message, "utf-8")) | ||
await self._writer.drain() | ||
|
||
async def _receive_response(self) -> str: | ||
assert isinstance(self._reader, asyncio.StreamReader) | ||
data = await self._reader.readline() | ||
return data.decode("utf-8") | ||
return codecs.decode(data, "utf-8") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I would prefer to have to do
than this honestly. It is more conventional and it allows minimising the time the lock is held without having to split it out into a function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The old implementation wrapped all the contents of the functions in the context manager though. I can see why this would be nice for if only part of the function was in the CM, but I feel like the decorator approach is cleaner if that isn't the case.