generated from daniil-berg/boilerplate-py
Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
5a72a6d1d1
|
|||
72e380cd77
|
|||
85672bddeb
|
|||
dae883446a
|
@ -22,7 +22,7 @@ copyright = '2022 Daniil Fajnberg'
|
||||
author = 'Daniil Fajnberg'
|
||||
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = '1.0.2'
|
||||
release = '1.1.2'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = asyncio-taskpool
|
||||
version = 1.0.3
|
||||
version = 1.1.2
|
||||
author = Daniil Fajnberg
|
||||
author_email = mail@daniil.fajnberg.de
|
||||
description = Dynamically manage pools of asyncio tasks
|
||||
|
@ -85,6 +85,7 @@ class ControlClient(ABC):
|
||||
"""
|
||||
self._connected = True
|
||||
writer.write(json.dumps(self._client_info()).encode())
|
||||
writer.write(b'\n')
|
||||
await writer.drain()
|
||||
print("Connected to", (await reader.read(SESSION_MSG_BYTES)).decode())
|
||||
print("Type '-h' to get help and usage instructions for all available commands.\n")
|
||||
@ -131,6 +132,7 @@ class ControlClient(ABC):
|
||||
try:
|
||||
# Send the command to the server.
|
||||
writer.write(cmd.encode())
|
||||
writer.write(b'\n')
|
||||
await writer.drain()
|
||||
except ConnectionError as e:
|
||||
self._connected = False
|
||||
|
@ -32,7 +32,7 @@ from typing import Callable, Optional, Union, TYPE_CHECKING
|
||||
from .parser import ControlParser
|
||||
from ..exceptions import CommandError, HelpRequested, ParserError
|
||||
from ..pool import TaskPool, SimpleTaskPool
|
||||
from ..internals.constants import CLIENT_INFO, CMD, CMD_OK, SESSION_MSG_BYTES
|
||||
from ..internals.constants import CLIENT_INFO, CMD, CMD_OK
|
||||
from ..internals.helpers import return_or_exception
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -134,7 +134,8 @@ class ControlSession:
|
||||
Client info is retrieved, server info is sent back, and the
|
||||
:class:`ControlParser <asyncio_taskpool.control.parser.ControlParser>` is set up.
|
||||
"""
|
||||
client_info = json.loads((await self._reader.read(SESSION_MSG_BYTES)).decode().strip())
|
||||
msg = (await self._reader.readline()).decode().strip()
|
||||
client_info = json.loads(msg)
|
||||
log.debug("%s connected", self._client_class_name)
|
||||
parser_kwargs = {
|
||||
'stream': self._response_buffer,
|
||||
@ -187,7 +188,7 @@ class ControlSession:
|
||||
It will obviously block indefinitely.
|
||||
"""
|
||||
while self._control_server.is_serving():
|
||||
msg = (await self._reader.read(SESSION_MSG_BYTES)).decode().strip()
|
||||
msg = (await self._reader.readline()).decode().strip()
|
||||
if not msg:
|
||||
log.debug("%s disconnected", self._client_class_name)
|
||||
break
|
||||
|
@ -31,7 +31,7 @@ import logging
|
||||
import warnings
|
||||
from asyncio.coroutines import iscoroutine, iscoroutinefunction
|
||||
from asyncio.exceptions import CancelledError
|
||||
from asyncio.locks import Semaphore
|
||||
from asyncio.locks import Event, Semaphore
|
||||
from asyncio.tasks import Task, create_task, gather
|
||||
from contextlib import suppress
|
||||
from math import inf
|
||||
@ -72,7 +72,7 @@ class BaseTaskPool:
|
||||
|
||||
# Initialize flags; immutably set the name.
|
||||
self._locked: bool = False
|
||||
self._closed: bool = False
|
||||
self._closed: Event = Event()
|
||||
self._name: str = name
|
||||
|
||||
# The following three dictionaries are the actual containers of the tasks controlled by the pool.
|
||||
@ -221,7 +221,7 @@ class BaseTaskPool:
|
||||
raise exceptions.NotCoroutine(f"Not awaitable: {awaitable}")
|
||||
if function and not iscoroutinefunction(function):
|
||||
raise exceptions.NotCoroutine(f"Not a coroutine function: {function}")
|
||||
if self._closed:
|
||||
if self._closed.is_set():
|
||||
raise exceptions.PoolIsClosed("You must use another pool")
|
||||
if self._locked and not ignore_lock:
|
||||
raise exceptions.PoolIsLocked("Cannot start new tasks")
|
||||
@ -550,9 +550,16 @@ class BaseTaskPool:
|
||||
self._tasks_ended.clear()
|
||||
self._tasks_cancelled.clear()
|
||||
self._tasks_running.clear()
|
||||
self._closed = True
|
||||
# TODO: Turn the `_closed` attribute into an `Event` and add something like a `until_closed` method that will
|
||||
# await it to allow blocking until a closing command comes from a server.
|
||||
self._closed.set()
|
||||
|
||||
async def until_closed(self) -> bool:
|
||||
"""
|
||||
Waits until the pool has been closed. (This method itself does **not** close the pool, but blocks until then.)
|
||||
|
||||
Returns:
|
||||
`True` once the pool is closed.
|
||||
"""
|
||||
return await self._closed.wait()
|
||||
|
||||
|
||||
class TaskPool(BaseTaskPool):
|
||||
@ -758,9 +765,10 @@ class TaskPool(BaseTaskPool):
|
||||
def _map(self, group_name: str, num_concurrent: int, func: CoroutineFunc, arg_iter: ArgsT, arg_stars: int,
|
||||
end_callback: EndCB = None, cancel_callback: CancelCB = None) -> None:
|
||||
"""
|
||||
Creates tasks in the pool with arguments from the supplied iterable.
|
||||
Creates coroutines with arguments from the supplied iterable and runs them as new tasks in the pool.
|
||||
|
||||
Each coroutine looks like `func(arg)`, `func(*arg)`, or `func(**arg)`, `arg` being taken from `arg_iter`.
|
||||
The method is a task-based equivalent of the `multiprocessing.pool.Pool.map` method.
|
||||
|
||||
All the new tasks are added to the same task group.
|
||||
|
||||
@ -812,10 +820,10 @@ class TaskPool(BaseTaskPool):
|
||||
def map(self, func: CoroutineFunc, arg_iter: ArgsT, num_concurrent: int = 1, group_name: str = None,
|
||||
end_callback: EndCB = None, cancel_callback: CancelCB = None) -> str:
|
||||
"""
|
||||
A task-based equivalent of the `multiprocessing.pool.Pool.map` method.
|
||||
|
||||
Creates coroutines with arguments from the supplied iterable and runs them as new tasks in the pool.
|
||||
Each coroutine looks like `func(arg)`, `arg` being an element taken from `arg_iter`.
|
||||
|
||||
Each coroutine looks like `func(arg)`, `arg` being an element taken from `arg_iter`. The method is a task-based
|
||||
equivalent of the `multiprocessing.pool.Pool.map` method.
|
||||
|
||||
All the new tasks are added to the same task group.
|
||||
|
||||
@ -869,6 +877,8 @@ class TaskPool(BaseTaskPool):
|
||||
def starmap(self, func: CoroutineFunc, args_iter: Iterable[ArgsT], num_concurrent: int = 1, group_name: str = None,
|
||||
end_callback: EndCB = None, cancel_callback: CancelCB = None) -> str:
|
||||
"""
|
||||
Creates coroutines with arguments from the supplied iterable and runs them as new tasks in the pool.
|
||||
|
||||
Like :meth:`map` except that the elements of `args_iter` are expected to be iterables themselves to be unpacked
|
||||
as positional arguments to the function.
|
||||
Each coroutine then looks like `func(*args)`, `args` being an element from `args_iter`.
|
||||
@ -886,6 +896,8 @@ class TaskPool(BaseTaskPool):
|
||||
def doublestarmap(self, func: CoroutineFunc, kwargs_iter: Iterable[KwArgsT], num_concurrent: int = 1,
|
||||
group_name: str = None, end_callback: EndCB = None, cancel_callback: CancelCB = None) -> str:
|
||||
"""
|
||||
Creates coroutines with arguments from the supplied iterable and runs them as new tasks in the pool.
|
||||
|
||||
Like :meth:`map` except that the elements of `kwargs_iter` are expected to be iterables themselves to be
|
||||
unpacked as keyword-arguments to the function.
|
||||
Each coroutine then looks like `func(**kwargs)`, `kwargs` being an element from `kwargs_iter`.
|
||||
|
@ -19,7 +19,7 @@ Unittests for the `asyncio_taskpool.pool` module.
|
||||
"""
|
||||
|
||||
from asyncio.exceptions import CancelledError
|
||||
from asyncio.locks import Semaphore
|
||||
from asyncio.locks import Event, Semaphore
|
||||
from unittest import IsolatedAsyncioTestCase
|
||||
from unittest.mock import PropertyMock, MagicMock, AsyncMock, patch, call
|
||||
from typing import Type
|
||||
@ -83,7 +83,8 @@ class BaseTaskPoolTestCase(CommonTestCase):
|
||||
self.assertEqual(0, self.task_pool._num_started)
|
||||
|
||||
self.assertFalse(self.task_pool._locked)
|
||||
self.assertFalse(self.task_pool._closed)
|
||||
self.assertIsInstance(self.task_pool._closed, Event)
|
||||
self.assertFalse(self.task_pool._closed.is_set())
|
||||
self.assertEqual(self.TEST_POOL_NAME, self.task_pool._name)
|
||||
|
||||
self.assertDictEqual(EMPTY_DICT, self.task_pool._tasks_running)
|
||||
@ -162,7 +163,7 @@ class BaseTaskPoolTestCase(CommonTestCase):
|
||||
self.task_pool.get_group_ids(group_name, 'something else')
|
||||
|
||||
async def test__check_start(self):
|
||||
self.task_pool._closed = True
|
||||
self.task_pool._closed.set()
|
||||
mock_coroutine, mock_coroutine_function = AsyncMock()(), AsyncMock()
|
||||
try:
|
||||
with self.assertRaises(AssertionError):
|
||||
@ -175,7 +176,7 @@ class BaseTaskPoolTestCase(CommonTestCase):
|
||||
self.task_pool._check_start(awaitable=None, function=mock_coroutine)
|
||||
with self.assertRaises(exceptions.PoolIsClosed):
|
||||
self.task_pool._check_start(awaitable=mock_coroutine, function=None)
|
||||
self.task_pool._closed = False
|
||||
self.task_pool._closed.clear()
|
||||
self.task_pool._locked = True
|
||||
with self.assertRaises(exceptions.PoolIsLocked):
|
||||
self.task_pool._check_start(awaitable=mock_coroutine, function=None, ignore_lock=False)
|
||||
@ -461,7 +462,13 @@ class BaseTaskPoolTestCase(CommonTestCase):
|
||||
self.assertDictEqual(EMPTY_DICT, self.task_pool._tasks_ended)
|
||||
self.assertDictEqual(EMPTY_DICT, self.task_pool._tasks_cancelled)
|
||||
self.assertDictEqual(EMPTY_DICT, self.task_pool._tasks_running)
|
||||
self.assertTrue(self.task_pool._closed)
|
||||
self.assertTrue(self.task_pool._closed.is_set())
|
||||
|
||||
async def test_until_closed(self):
|
||||
self.task_pool._closed = MagicMock(wait=AsyncMock(return_value=FOO))
|
||||
output = await self.task_pool.until_closed()
|
||||
self.assertEqual(FOO, output)
|
||||
self.task_pool._closed.wait.assert_awaited_once_with()
|
||||
|
||||
|
||||
class TaskPoolTestCase(CommonTestCase):
|
||||
|
Reference in New Issue
Block a user