anyio-executor 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: anyio-executor
3
+ Version: 0.2.0
4
+ Summary: anyio implementation of an asynchronous Executor.
5
+ Author-email: Vizonex <VizonexBusiness@gmail.com>
6
+ Project-URL: repository, https://github.com/Vizonex/anyio-executor.git
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: aiologic>=0.17.1
11
+ Requires-Dist: anyio>=4.16.0
12
+ Requires-Dist: typing_extensions; python_version < "3.15"
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest; extra == "test"
15
+ Provides-Extra: test-advanced
16
+ Requires-Dist: uvloop; platform_system != "Windows" and extra == "test-advanced"
17
+ Requires-Dist: winloop; platform_system == "Windows" and extra == "test-advanced"
18
+ Requires-Dist: trio; extra == "test-advanced"
19
+ Requires-Dist: pytest; extra == "test-advanced"
20
+ Dynamic: license-file
21
+
22
+ # Anyio-Executor
23
+ Inspired by the newer [aiolibs-executor](https://github.com/aio-libs/aiolibs-executor) library
24
+ this library provides a asynchronous executor for anyio and has compatability
25
+ with 3.11 and newer versions of python.
26
+
27
+
28
+ ## Installation
29
+ ```
30
+ pip install anyio-executor
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ It borrows a simillar interface to that of [aiothreading](https://github.com/Vizonex/aiothreading)
36
+ however this library is a lot more simplistic as an external thread is not being launched.
37
+ As for queues, anyio doesn't support having it's own queue objects so in order to meet demands for
38
+ excellent performance [aiologic](https://github.com/x42005e1f/aiologic) is utilized for that role.
39
+
40
+ ```python
41
+ import anyio
42
+
43
+ from anyio_executor import Executor
44
+
45
+
46
+ async def fn(i: int):
47
+ await anyio.sleep(i)
48
+ print(f"{i} done!")
49
+ return i
50
+
51
+
52
+ async def main():
53
+ async with Executor(10) as e:
54
+ items = await e.map(fn, [1, 2, 1, 2, 3])
55
+ print(items)
56
+
57
+ async with Executor(10) as e:
58
+ # it will also take async for iterations.
59
+ async for i in e.map(fn, [1, 2, 1, 2, 3]):
60
+ print(i)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ anyio.run(main)
65
+ ```
@@ -1,9 +1,21 @@
1
1
  # Anyio-Executor
2
2
  Inspired by the newer [aiolibs-executor](https://github.com/aio-libs/aiolibs-executor) library
3
- this library provides an asynchronous executor support for anyio and has compatability
4
- with 3.10 and newer version of python.
3
+ this library provides a asynchronous executor for anyio and has compatability
4
+ with 3.11 and newer versions of python.
5
5
 
6
6
 
7
+ ## Installation
8
+ ```
9
+ pip install anyio-executor
10
+ ```
11
+
12
+ ## Usage
13
+
14
+ It borrows a simillar interface to that of [aiothreading](https://github.com/Vizonex/aiothreading)
15
+ however this library is a lot more simplistic as an external thread is not being launched.
16
+ As for queues, anyio doesn't support having it's own queue objects so in order to meet demands for
17
+ excellent performance [aiologic](https://github.com/x42005e1f/aiologic) is utilized for that role.
18
+
7
19
  ```python
8
20
  import anyio
9
21
 
@@ -15,6 +27,7 @@ async def fn(i: int):
15
27
  print(f"{i} done!")
16
28
  return i
17
29
 
30
+
18
31
  async def main():
19
32
  async with Executor(10) as e:
20
33
  items = await e.map(fn, [1, 2, 1, 2, 3])
@@ -25,6 +38,7 @@ async def main():
25
38
  async for i in e.map(fn, [1, 2, 1, 2, 3]):
26
39
  print(i)
27
40
 
41
+
28
42
  if __name__ == "__main__":
29
43
  anyio.run(main)
30
44
  ```
@@ -6,10 +6,11 @@ readme = "README.md"
6
6
  authors = [
7
7
  { name = "Vizonex", email = "VizonexBusiness@gmail.com" }
8
8
  ]
9
- requires-python = ">=3.10"
9
+ requires-python = ">=3.11"
10
10
  dependencies = [
11
- "aiologic>=0.16.0",
12
- "anyio>=4.13.0",
11
+ "aiologic>=0.17.1",
12
+ "anyio>=4.16.0",
13
+ 'typing_extensions; python_version < "3.15"',
13
14
  ]
14
15
  [build-system]
15
16
  requires = ["setuptools"]
@@ -17,6 +18,12 @@ requires = ["setuptools"]
17
18
 
18
19
  [project.optional-dependencies]
19
20
  test = ["pytest"]
21
+ test-advanced = [
22
+ 'uvloop; platform_system != "Windows"',
23
+ 'winloop; platform_system == "Windows"',
24
+ 'trio',
25
+ 'pytest',
26
+ ]
20
27
 
21
28
  [project.urls]
22
29
  repository = "https://github.com/Vizonex/anyio-executor.git"
@@ -26,7 +33,7 @@ test = ["pytest"]
26
33
  version = {attr = "anyio_executor.__version__"}
27
34
 
28
35
  [tool.ruff.lint]
29
- select = ["E", "I", "UP", "ASYNC"]
36
+ select = ["E", "I", "UP", "ASYNC", "F", "RUF"]
30
37
 
31
38
  [tool.ruff.lint.isort]
32
39
  combine-as-imports = true
@@ -1,6 +1,6 @@
1
1
  from .core import Executor, ExecutorResult
2
2
 
3
3
 
4
- __version__ = "0.1.0"
4
+ __version__ = "0.2.0"
5
5
  __author__ = "Vizonex"
6
6
  __all__ = ("Executor", "ExecutorResult")
@@ -1,13 +1,7 @@
1
- import sys
2
1
  from collections.abc import AsyncIterable, AsyncIterator
3
- from typing import Any, Generic, TypeVar, overload
2
+ from typing import Any, TypeVar, overload
4
3
 
5
4
 
6
- if sys.version_info < (3, 11):
7
- from typing_extensions import Self
8
- else:
9
- from typing import Self
10
-
11
5
  # There are other asynchronous azip libraries out there
12
6
  # but the maitnence of those can be questioned so here's
13
7
  # our own version
@@ -7,14 +7,13 @@ from collections.abc import (
7
7
  Coroutine,
8
8
  Generator,
9
9
  Iterable,
10
- Iterator,
11
10
  Sequence,
12
11
  )
13
12
  from contextlib import contextmanager
14
13
  from dataclasses import dataclass
15
14
  from functools import partial
16
15
  from types import TracebackType
17
- from typing import Any, Generic, TypeVar, final, overload
16
+ from typing import Any, Generic, Self, TypeVar, final, overload
18
17
 
19
18
  import anyio
20
19
  from aiologic import CountdownEvent, SimpleQueue
@@ -22,11 +21,8 @@ from aiologic import CountdownEvent, SimpleQueue
22
21
  from .azip import azip
23
22
 
24
23
 
25
- if sys.version_info < (3, 11):
26
- from typing_extensions import Self
27
- else:
28
- from typing import Self
29
-
24
+ if sys.version_info < (3, 15):
25
+ from typing_extensions import Sentinel as sentinel
30
26
 
31
27
  R = TypeVar("R")
32
28
  T = TypeVar("T")
@@ -36,12 +32,15 @@ T3 = TypeVar("T3")
36
32
  T4 = TypeVar("T4")
37
33
  T5 = TypeVar("T5")
38
34
 
35
+ MISSING = sentinel("MISSING")
36
+ """Internal variable not meant to be used publically,
37
+ meant to mark results ran with an exeception handler."""
39
38
 
40
- @dataclass(slots=True)
39
+ @dataclass(frozen=True, slots=True)
41
40
  class _WorkItem(Generic[T]):
42
41
  func: Callable[[], Awaitable[T]]
43
42
  exc_handler: Callable[[BaseException], None] | None
44
- queue: SimpleQueue[T]
43
+ queue: SimpleQueue[T | MISSING]
45
44
 
46
45
  async def execute(self) -> None:
47
46
  try:
@@ -49,26 +48,30 @@ class _WorkItem(Generic[T]):
49
48
  except Exception as e:
50
49
  if self.exc_handler:
51
50
  self.exc_handler(e)
51
+ # mark as work done even though there is no objects.
52
+ await self.queue.async_put(MISSING)
52
53
  else:
53
54
  raise e
54
55
 
55
56
 
56
- @dataclass(slots=True)
57
+ @dataclass(frozen=True, slots=True)
57
58
  class ExecutorResult(Awaitable[list[T]], AsyncIterable[T]):
58
59
  """used for waiting upon results to return in different asynchronous
59
- fashions."""
60
+ fashions. It is designed as a hybrid ot be awaited or used in
61
+ async generator fashion."""
60
62
 
61
- queue: SimpleQueue[T]
63
+ queue: SimpleQueue[T | MISSING]
62
64
  count: CountdownEvent
63
65
  executor: "Executor"
64
66
 
65
67
  async def __aiter__(self) -> AsyncIterator[T]:
66
68
  while not self.executor.is_closed:
67
69
  item = await self.queue.async_get()
68
- yield item
70
+ if item is not MISSING:
71
+ yield item
69
72
 
70
73
  self.count.down()
71
- if bool(self.count):
74
+ if self.count:
72
75
  break
73
76
 
74
77
  async def __wait__(self) -> list[T]:
@@ -87,10 +90,10 @@ class Executor:
87
90
 
88
91
  __slots__ = (
89
92
  "__weakref__",
90
- "_num_workers",
91
- "_work_queue",
92
93
  "_exc_handler",
94
+ "_num_workers",
93
95
  "_tg",
96
+ "_work_queue",
94
97
  "_workers_done",
95
98
  )
96
99
 
@@ -133,7 +136,7 @@ class Executor:
133
136
  return self
134
137
 
135
138
  @contextmanager
136
- def _worker_scope(self) -> Iterator[None]:
139
+ def _worker_scope(self) -> Generator[None, None, None]:
137
140
  """Assistance with Lazy event up/down event functionality."""
138
141
  self._workers_done.up()
139
142
  try:
@@ -278,7 +281,7 @@ class Executor:
278
281
  raise RuntimeError("Executor already closed.")
279
282
  cd = CountdownEvent()
280
283
  cd.up()
281
- queue: SimpleQueue[T] = SimpleQueue()
284
+ queue: SimpleQueue[T | MISSING] = SimpleQueue()
282
285
  self._tg.start_soon(
283
286
  self._load_tasks_sync_iter,
284
287
  queue,
@@ -376,10 +379,9 @@ class Executor:
376
379
  if self.is_closed:
377
380
  raise RuntimeError("Executor already closed.")
378
381
 
379
- queue: SimpleQueue[T] = SimpleQueue()
380
382
  cd = CountdownEvent()
381
383
  cd.up()
382
- queue: SimpleQueue[T] = SimpleQueue()
384
+ queue: SimpleQueue[T | MISSING] = SimpleQueue()
383
385
  self._tg.start_soon(
384
386
  self._load_tasks_async_iter,
385
387
  queue,
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: anyio-executor
3
+ Version: 0.2.0
4
+ Summary: anyio implementation of an asynchronous Executor.
5
+ Author-email: Vizonex <VizonexBusiness@gmail.com>
6
+ Project-URL: repository, https://github.com/Vizonex/anyio-executor.git
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: aiologic>=0.17.1
11
+ Requires-Dist: anyio>=4.16.0
12
+ Requires-Dist: typing_extensions; python_version < "3.15"
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest; extra == "test"
15
+ Provides-Extra: test-advanced
16
+ Requires-Dist: uvloop; platform_system != "Windows" and extra == "test-advanced"
17
+ Requires-Dist: winloop; platform_system == "Windows" and extra == "test-advanced"
18
+ Requires-Dist: trio; extra == "test-advanced"
19
+ Requires-Dist: pytest; extra == "test-advanced"
20
+ Dynamic: license-file
21
+
22
+ # Anyio-Executor
23
+ Inspired by the newer [aiolibs-executor](https://github.com/aio-libs/aiolibs-executor) library
24
+ this library provides a asynchronous executor for anyio and has compatability
25
+ with 3.11 and newer versions of python.
26
+
27
+
28
+ ## Installation
29
+ ```
30
+ pip install anyio-executor
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ It borrows a simillar interface to that of [aiothreading](https://github.com/Vizonex/aiothreading)
36
+ however this library is a lot more simplistic as an external thread is not being launched.
37
+ As for queues, anyio doesn't support having it's own queue objects so in order to meet demands for
38
+ excellent performance [aiologic](https://github.com/x42005e1f/aiologic) is utilized for that role.
39
+
40
+ ```python
41
+ import anyio
42
+
43
+ from anyio_executor import Executor
44
+
45
+
46
+ async def fn(i: int):
47
+ await anyio.sleep(i)
48
+ print(f"{i} done!")
49
+ return i
50
+
51
+
52
+ async def main():
53
+ async with Executor(10) as e:
54
+ items = await e.map(fn, [1, 2, 1, 2, 3])
55
+ print(items)
56
+
57
+ async with Executor(10) as e:
58
+ # it will also take async for iterations.
59
+ async for i in e.map(fn, [1, 2, 1, 2, 3]):
60
+ print(i)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ anyio.run(main)
65
+ ```
@@ -0,0 +1,18 @@
1
+ aiologic>=0.17.1
2
+ anyio>=4.16.0
3
+
4
+ [:python_version < "3.15"]
5
+ typing_extensions
6
+
7
+ [test]
8
+ pytest
9
+
10
+ [test-advanced]
11
+ trio
12
+ pytest
13
+
14
+ [test-advanced:platform_system != "Windows"]
15
+ uvloop
16
+
17
+ [test-advanced:platform_system == "Windows"]
18
+ winloop
@@ -1,3 +1,6 @@
1
+ from collections.abc import Callable
2
+ from unittest import mock
3
+
1
4
  import anyio
2
5
  import pytest
3
6
 
@@ -16,17 +19,22 @@ class BaseExecutorTest:
16
19
  def executor(self) -> Executor:
17
20
  return Executor()
18
21
 
22
+ def executor_with_handler(
23
+ self, handler:Callable[[BaseException], None]
24
+ ) -> Executor:
25
+ return Executor(exc_handler=handler)
26
+
19
27
 
20
28
  class TestInvalidStates(BaseExecutorTest):
21
29
  @pytest.mark.anyio
22
30
  async def test_invalid_worker_amount(self):
23
- with pytest.raises(ValueError, match="num_wokers must be a positive integer"):
31
+ with pytest.raises(ValueError, match=r"num_wokers must be a positive integer"):
24
32
  _ = Executor(-69)
25
33
 
26
34
  @pytest.mark.anyio
27
35
  async def test_bad_map_before_entrance(self):
28
36
  e = self.executor()
29
- with pytest.raises(RuntimeError, match="Executor already closed."):
37
+ with pytest.raises(RuntimeError, match=r"Executor already closed."):
30
38
  await e.map(noop, [1, 2, 3])
31
39
 
32
40
  @pytest.mark.anyio
@@ -37,14 +45,14 @@ class TestInvalidStates(BaseExecutorTest):
37
45
  for i in range(3):
38
46
  yield i
39
47
 
40
- with pytest.raises(RuntimeError, match="Executor already closed."):
48
+ with pytest.raises(RuntimeError, match=r"Executor already closed."):
41
49
  await e.amap(noop, fake_aiter())
42
50
 
43
51
  @pytest.mark.anyio
44
52
  async def test_bad_map_after_shutdown(self):
45
53
  async with self.executor() as e:
46
54
  await e.shutdown()
47
- with pytest.raises(RuntimeError, match="Executor already closed."):
55
+ with pytest.raises(RuntimeError, match=r"Executor already closed."):
48
56
  await e.map(noop, [1, 2, 3])
49
57
 
50
58
  @pytest.mark.anyio
@@ -56,9 +64,10 @@ class TestInvalidStates(BaseExecutorTest):
56
64
  for i in range(3):
57
65
  yield i
58
66
 
59
- with pytest.raises(RuntimeError, match="Executor already closed."):
67
+ with pytest.raises(RuntimeError, match=r"Executor already closed."):
60
68
  await e.amap(noop, fake_aiter())
61
69
 
70
+ @pytest.mark.anyio
62
71
 
63
72
  class TestExecutor(BaseExecutorTest):
64
73
  async def wait(self, item: int) -> int:
@@ -70,7 +79,9 @@ class TestExecutor(BaseExecutorTest):
70
79
  async with self.executor() as e:
71
80
  items = [i for i in range(1, 5)]
72
81
  i = await e.map(self.wait, items)
73
- assert i == [1, 2, 3, 4]
82
+ # it doesn't matter what order they come back
83
+ # in (as long as they all come back of course)
84
+ assert set(i) == {1, 2, 3, 4}
74
85
 
75
86
  @pytest.mark.anyio
76
87
  async def test_map_aiter(self):
@@ -79,6 +90,10 @@ class TestExecutor(BaseExecutorTest):
79
90
  async for i in e.map(fast_ret, items):
80
91
  assert i in [1, 2, 3, 4]
81
92
 
93
+ # NOTE: Sometimes trio will attempt to execute things
94
+ # differently example: [4, 1, 2, 3]
95
+ # as long as they all come return back to us then it did
96
+ # it's job.
82
97
  @pytest.mark.anyio
83
98
  async def test_amap_await(self):
84
99
  async def ait():
@@ -87,7 +102,7 @@ class TestExecutor(BaseExecutorTest):
87
102
 
88
103
  async with self.executor() as e:
89
104
  i = await e.amap(self.wait, ait())
90
- assert i == [1, 2, 3, 4]
105
+ assert set(i) == {1, 2, 3, 4}
91
106
 
92
107
  @pytest.mark.anyio
93
108
  async def test_amap_aiter(self):
@@ -98,3 +113,26 @@ class TestExecutor(BaseExecutorTest):
98
113
  async with self.executor() as e:
99
114
  async for i in e.amap(fast_ret, ait()):
100
115
  assert i in [1, 2, 3, 4]
116
+
117
+ @pytest.mark.anyio
118
+ async def test_exception_handler(self):
119
+ callback = mock.Mock()
120
+
121
+ class TornadoError(Exception):
122
+ pass
123
+
124
+ def on_exception(exc: Exception):
125
+ assert isinstance(exc, TornadoError)
126
+ callback(exc)
127
+
128
+ async def func(i: int):
129
+ raise TornadoError("Tornado approching...")
130
+
131
+ async with self.executor_with_handler(on_exception) as e:
132
+ ret = await e.map(func, (1,))
133
+ assert not ret
134
+ assert callback.called
135
+ assert callback.call_args.args
136
+ assert isinstance(callback.call_args.args[0], TornadoError)
137
+
138
+
@@ -1,45 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: anyio-executor
3
- Version: 0.1.0
4
- Summary: anyio implementation of an asynchronous Executor.
5
- Author-email: Vizonex <VizonexBusiness@gmail.com>
6
- Project-URL: repository, https://github.com/Vizonex/anyio-executor.git
7
- Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
- License-File: LICENSE
10
- Requires-Dist: aiologic>=0.16.0
11
- Requires-Dist: anyio>=4.13.0
12
- Provides-Extra: test
13
- Requires-Dist: pytest; extra == "test"
14
- Dynamic: license-file
15
-
16
- # Anyio-Executor
17
- Inspired by the newer [aiolibs-executor](https://github.com/aio-libs/aiolibs-executor) library
18
- this library provides an asynchronous executor support for anyio and has compatability
19
- with 3.10 and newer version of python.
20
-
21
-
22
- ```python
23
- import anyio
24
-
25
- from anyio_executor import Executor
26
-
27
-
28
- async def fn(i: int):
29
- await anyio.sleep(i)
30
- print(f"{i} done!")
31
- return i
32
-
33
- async def main():
34
- async with Executor(10) as e:
35
- items = await e.map(fn, [1, 2, 1, 2, 3])
36
- print(items)
37
-
38
- async with Executor(10) as e:
39
- # it will also take async for iterations.
40
- async for i in e.map(fn, [1, 2, 1, 2, 3]):
41
- print(i)
42
-
43
- if __name__ == "__main__":
44
- anyio.run(main)
45
- ```
@@ -1,45 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: anyio-executor
3
- Version: 0.1.0
4
- Summary: anyio implementation of an asynchronous Executor.
5
- Author-email: Vizonex <VizonexBusiness@gmail.com>
6
- Project-URL: repository, https://github.com/Vizonex/anyio-executor.git
7
- Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
- License-File: LICENSE
10
- Requires-Dist: aiologic>=0.16.0
11
- Requires-Dist: anyio>=4.13.0
12
- Provides-Extra: test
13
- Requires-Dist: pytest; extra == "test"
14
- Dynamic: license-file
15
-
16
- # Anyio-Executor
17
- Inspired by the newer [aiolibs-executor](https://github.com/aio-libs/aiolibs-executor) library
18
- this library provides an asynchronous executor support for anyio and has compatability
19
- with 3.10 and newer version of python.
20
-
21
-
22
- ```python
23
- import anyio
24
-
25
- from anyio_executor import Executor
26
-
27
-
28
- async def fn(i: int):
29
- await anyio.sleep(i)
30
- print(f"{i} done!")
31
- return i
32
-
33
- async def main():
34
- async with Executor(10) as e:
35
- items = await e.map(fn, [1, 2, 1, 2, 3])
36
- print(items)
37
-
38
- async with Executor(10) as e:
39
- # it will also take async for iterations.
40
- async for i in e.map(fn, [1, 2, 1, 2, 3]):
41
- print(i)
42
-
43
- if __name__ == "__main__":
44
- anyio.run(main)
45
- ```
@@ -1,5 +0,0 @@
1
- aiologic>=0.16.0
2
- anyio>=4.13.0
3
-
4
- [test]
5
- pytest
File without changes
File without changes