anyio-executor 0.1.1__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.
@@ -1,14 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anyio-executor
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: anyio implementation of an asynchronous Executor.
5
5
  Author-email: Vizonex <VizonexBusiness@gmail.com>
6
6
  Project-URL: repository, https://github.com/Vizonex/anyio-executor.git
7
- Requires-Python: >=3.10
7
+ Requires-Python: >=3.11
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
- Requires-Dist: aiologic>=0.16.0
11
- Requires-Dist: anyio>=4.13.0
10
+ Requires-Dist: aiologic>=0.17.1
11
+ Requires-Dist: anyio>=4.16.0
12
+ Requires-Dist: typing_extensions; python_version < "3.15"
12
13
  Provides-Extra: test
13
14
  Requires-Dist: pytest; extra == "test"
14
15
  Provides-Extra: test-advanced
@@ -21,7 +22,7 @@ Dynamic: license-file
21
22
  # Anyio-Executor
22
23
  Inspired by the newer [aiolibs-executor](https://github.com/aio-libs/aiolibs-executor) library
23
24
  this library provides a asynchronous executor for anyio and has compatability
24
- with 3.10 and newer versions of python.
25
+ with 3.11 and newer versions of python.
25
26
 
26
27
 
27
28
  ## Installation
@@ -47,6 +48,7 @@ async def fn(i: int):
47
48
  print(f"{i} done!")
48
49
  return i
49
50
 
51
+
50
52
  async def main():
51
53
  async with Executor(10) as e:
52
54
  items = await e.map(fn, [1, 2, 1, 2, 3])
@@ -57,6 +59,7 @@ async def main():
57
59
  async for i in e.map(fn, [1, 2, 1, 2, 3]):
58
60
  print(i)
59
61
 
62
+
60
63
  if __name__ == "__main__":
61
64
  anyio.run(main)
62
65
  ```
@@ -1,7 +1,7 @@
1
1
  # Anyio-Executor
2
2
  Inspired by the newer [aiolibs-executor](https://github.com/aio-libs/aiolibs-executor) library
3
3
  this library provides a asynchronous executor for anyio and has compatability
4
- with 3.10 and newer versions of python.
4
+ with 3.11 and newer versions of python.
5
5
 
6
6
 
7
7
  ## Installation
@@ -27,6 +27,7 @@ async def fn(i: int):
27
27
  print(f"{i} done!")
28
28
  return i
29
29
 
30
+
30
31
  async def main():
31
32
  async with Executor(10) as e:
32
33
  items = await e.map(fn, [1, 2, 1, 2, 3])
@@ -37,6 +38,7 @@ async def main():
37
38
  async for i in e.map(fn, [1, 2, 1, 2, 3]):
38
39
  print(i)
39
40
 
41
+
40
42
  if __name__ == "__main__":
41
43
  anyio.run(main)
42
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"]
@@ -1,6 +1,6 @@
1
1
  from .core import Executor, ExecutorResult
2
2
 
3
3
 
4
- __version__ = "0.1.1"
4
+ __version__ = "0.2.0"
5
5
  __author__ = "Vizonex"
6
6
  __all__ = ("Executor", "ExecutorResult")
@@ -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
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,6 +48,8 @@ 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
 
@@ -56,19 +57,21 @@ class _WorkItem(Generic[T]):
56
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]:
@@ -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,
@@ -1,14 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: anyio-executor
3
- Version: 0.1.1
3
+ Version: 0.2.0
4
4
  Summary: anyio implementation of an asynchronous Executor.
5
5
  Author-email: Vizonex <VizonexBusiness@gmail.com>
6
6
  Project-URL: repository, https://github.com/Vizonex/anyio-executor.git
7
- Requires-Python: >=3.10
7
+ Requires-Python: >=3.11
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
- Requires-Dist: aiologic>=0.16.0
11
- Requires-Dist: anyio>=4.13.0
10
+ Requires-Dist: aiologic>=0.17.1
11
+ Requires-Dist: anyio>=4.16.0
12
+ Requires-Dist: typing_extensions; python_version < "3.15"
12
13
  Provides-Extra: test
13
14
  Requires-Dist: pytest; extra == "test"
14
15
  Provides-Extra: test-advanced
@@ -21,7 +22,7 @@ Dynamic: license-file
21
22
  # Anyio-Executor
22
23
  Inspired by the newer [aiolibs-executor](https://github.com/aio-libs/aiolibs-executor) library
23
24
  this library provides a asynchronous executor for anyio and has compatability
24
- with 3.10 and newer versions of python.
25
+ with 3.11 and newer versions of python.
25
26
 
26
27
 
27
28
  ## Installation
@@ -47,6 +48,7 @@ async def fn(i: int):
47
48
  print(f"{i} done!")
48
49
  return i
49
50
 
51
+
50
52
  async def main():
51
53
  async with Executor(10) as e:
52
54
  items = await e.map(fn, [1, 2, 1, 2, 3])
@@ -57,6 +59,7 @@ async def main():
57
59
  async for i in e.map(fn, [1, 2, 1, 2, 3]):
58
60
  print(i)
59
61
 
62
+
60
63
  if __name__ == "__main__":
61
64
  anyio.run(main)
62
65
  ```
@@ -1,5 +1,8 @@
1
- aiologic>=0.16.0
2
- anyio>=4.13.0
1
+ aiologic>=0.17.1
2
+ anyio>=4.16.0
3
+
4
+ [:python_version < "3.15"]
5
+ typing_extensions
3
6
 
4
7
  [test]
5
8
  pytest
@@ -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,7 @@ 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
- # it doesn't matter what order they come back
82
+ # it doesn't matter what order they come back
74
83
  # in (as long as they all come back of course)
75
84
  assert set(i) == {1, 2, 3, 4}
76
85
 
@@ -83,8 +92,8 @@ class TestExecutor(BaseExecutorTest):
83
92
 
84
93
  # NOTE: Sometimes trio will attempt to execute things
85
94
  # differently example: [4, 1, 2, 3]
86
- # as long as they all come return back to us then it did
87
- # it's job.
95
+ # as long as they all come return back to us then it did
96
+ # it's job.
88
97
  @pytest.mark.anyio
89
98
  async def test_amap_await(self):
90
99
  async def ait():
@@ -104,3 +113,26 @@ class TestExecutor(BaseExecutorTest):
104
113
  async with self.executor() as e:
105
114
  async for i in e.amap(fast_ret, ait()):
106
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
+
File without changes
File without changes