Skip to content

Commit 3b03611

Browse files
committed
Add Event Loop pattern
1 parent 6922174 commit 3b03611

File tree

16 files changed

+266
-76
lines changed

16 files changed

+266
-76
lines changed

src/behavioral/generator.py

Lines changed: 41 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
from __future__ import annotations
1313

14-
import io
1514
from abc import ABC, abstractmethod
1615
from sqlite3 import Connection
1716
from types import TracebackType
@@ -20,22 +19,6 @@
2019
from src.behavioral.iterator import Iterator
2120

2221

23-
class Generator[YieldT, SendT, ReturnT](Iterator[YieldT], ABC):
24-
@abstractmethod
25-
def send(self, value: SendT) -> YieldT: ...
26-
27-
@abstractmethod
28-
def throw(
29-
self,
30-
exc_type: type[BaseException],
31-
exc_val: BaseException | None = None,
32-
tb: TracebackType | None = None,
33-
) -> Self: ...
34-
35-
@abstractmethod
36-
def close(self) -> None: ...
37-
38-
3922
# Basic usage
4023

4124

@@ -72,9 +55,50 @@ def gen_sum() -> PythonGenerator[int, int, int]:
7255
return total
7356

7457

58+
class CommitException(Exception):
59+
pass
60+
61+
62+
class AbortException(Exception):
63+
pass
64+
65+
66+
def db_session(
67+
db: Connection, sql: str
68+
) -> PythonGenerator[None, tuple[Any, ...], None]:
69+
cursor = db.cursor()
70+
try:
71+
while True:
72+
try:
73+
row = yield
74+
cursor.execute(sql, row)
75+
except CommitException:
76+
db.commit()
77+
except AbortException:
78+
db.rollback()
79+
finally:
80+
db.rollback()
81+
82+
7583
# Class-based generator
7684

7785

86+
class Generator[YieldT, SendT, ReturnT](Iterator[YieldT], ABC):
87+
@abstractmethod
88+
def send(self, value: SendT) -> YieldT: ...
89+
90+
@abstractmethod
91+
def throw(
92+
self,
93+
exc_type: type[BaseException],
94+
exc_val: BaseException | None = None,
95+
tb: TracebackType | None = None,
96+
) -> Self: ...
97+
98+
@abstractmethod
99+
def close(self) -> None: ...
100+
101+
78102
class SumGenerator(Generator[int, int, int]):
79103
def __init__(self) -> None:
80104
self._total = 0
@@ -102,43 +126,3 @@ def __iter__(self) -> SumGenerator:
102126

103127
def __next__(self) -> int:
104128
return self.send(0)
105-
106-
107-
def gen_line(
108-
output: io.StringIO, state: dict[str, Any]
109-
) -> PythonGenerator[str, None, None]:
110-
# lines
111-
try:
112-
while True:
113-
line = output.readline().rstrip()
114-
if not line:
115-
break
116-
yield line
117-
finally:
118-
state["closed"] = True
119-
output.close()
120-
121-
122-
class CommitException(Exception):
123-
pass
124-
125-
126-
class AbortException(Exception):
127-
pass
128-
129-
130-
def db_session(
131-
db: Connection, sql: str
132-
) -> PythonGenerator[None, tuple[Any, ...], None]:
133-
cursor = db.cursor()
134-
try:
135-
while True:
136-
try:
137-
row = yield
138-
cursor.execute(sql, row)
139-
except CommitException:
140-
db.commit()
141-
except AbortException:
142-
db.rollback()
143-
finally:
144-
db.rollback()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# @coroutine decorator as asyncio.run

src/concurrency/patterns/asyncio.py

Whitespace-only changes.

src/concurrency/patterns/coroutine.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from abc import abstractmethod, ABC
2+
from typing import Any, Generator
3+
4+
5+
GeneratorCoroutine = Generator
6+
7+
8+
class Awaitable[T](ABC):
9+
@abstractmethod
10+
def __await__(self) -> Generator[Any, Any, T]: ...
11+
12+
13+
class AwaitableCoroutine[YieldT, SendT, ReturnT](
14+
Awaitable[ReturnT], Generator[YieldT, SendT, ReturnT], ABC
15+
):
16+
@abstractmethod
17+
def __await__(self) -> Generator[YieldT, SendT, ReturnT]: ...
Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,99 @@
1-
# https://medium.com/@pekelny/fake-event-loop-python3-7498761af5e0
1+
"""
2+
Event Loop is a concurrency design pattern that is used to handle asynchronous events in a program. It is a loop that
3+
listens for events and then triggers the appropriate event handlers. The Event Loop pattern is commonly used in GUI
4+
applications, web servers, and other programs that need to handle multiple events simultaneously.
5+
6+
A Task is a subclass of Future that represents a coroutine that is running in the event loop. It is used to
7+
manage the execution of the coroutine and handle its result.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import logging
13+
from queue import Queue
14+
from typing import Any
15+
16+
from src.concurrency.patterns.coroutine import GeneratorCoroutine
17+
from src.concurrency.patterns.future import Future
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
class Task[T](Future[T]):
23+
def __init__(
24+
self, gen: GeneratorCoroutine[Any, Any, T], loop: EventLoop
25+
) -> None:
26+
super().__init__()
27+
self.gen: GeneratorCoroutine[Any, Any, T] = gen
28+
self.loop: EventLoop = loop
29+
loop.put(self)
30+
31+
def step(self, value: Any = None) -> None:
32+
# Resume the coroutine
33+
try:
34+
yielded = self.gen.send(value)
35+
# If the coroutine yielded a Future, add a callback to resume the coroutine when the Future is done
36+
if isinstance(yielded, Future):
37+
yielded.add_done_callback(lambda fut: self.step(fut.result()))
38+
else:
39+
self.loop.put(self)
40+
# Coroutine has finished
41+
except StopIteration as e:
42+
self.set_result(e.value)
43+
# Coroutine raised an exception
44+
except Exception as e:
45+
self.set_exception(e)
46+
47+
48+
class EventLoop:
49+
def __init__(self) -> None:
50+
self.q: Queue[Task[Any]] = Queue()
51+
52+
def put(self, task: Task[Any]) -> None:
53+
self.q.put(task)
54+
55+
def run_until_complete[T](
56+
self, coro: GeneratorCoroutine[Any, Any, T]
57+
) -> T:
58+
task = create_task(coro)
59+
while not task.done():
60+
if not self.q.empty():
61+
next_task = self.q.get()
62+
next_task.step()
63+
return task.result()
64+
65+
def close(self) -> None:
66+
self.q.queue.clear()
67+
68+
69+
running_loop: EventLoop | None = None
70+
71+
72+
def new_event_loop() -> EventLoop:
73+
return EventLoop()
74+
75+
76+
def get_running_loop() -> EventLoop | None:
77+
return running_loop
78+
79+
80+
def get_event_loop() -> EventLoop:
81+
loop = get_running_loop()
82+
if loop is not None:
83+
return loop
84+
return new_event_loop()
85+
86+
87+
def create_task[T](coro: GeneratorCoroutine[Any, Any, T]) -> Task[T]:
88+
return Task(coro, get_event_loop())
89+
90+
91+
def run[T](coro: GeneratorCoroutine[Any, Any, T]) -> T:
92+
global running_loop
93+
loop = get_event_loop()
94+
running_loop = loop
95+
try:
96+
return loop.run_until_complete(coro)
97+
finally:
98+
running_loop = None
99+
loop.close()

src/concurrency/patterns/future.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""
2+
A Future is an object that represents the result of an asynchronous operation. It is used to store the result
3+
of the operation and notify the caller when the operation is complete.
4+
"""
5+
6+
from __future__ import annotations
7+
8+
from typing import Callable, Generator
9+
from src.concurrency.patterns.coroutine import Awaitable
10+
11+
12+
class Future[T](Awaitable[T]):
13+
def __init__(self) -> None:
14+
self._done: bool = False
15+
self._result: T | None = None
16+
self._exception: BaseException | None = None
17+
self._callbacks: list[Callable[[Future[T]], None]] = []
18+
19+
def done(self) -> bool:
20+
return self._done
21+
22+
def result(self) -> T:
23+
if not self._done:
24+
raise RuntimeError("Future is not done yet")
25+
if self._exception:
26+
raise self._exception
27+
return self._result # type: ignore
28+
29+
def set_result(self, result: T) -> None:
30+
self._result = result
31+
self._done = True
32+
self._schedule_callbacks()
33+
34+
def set_exception(self, exception: BaseException) -> None:
35+
self._exception = exception
36+
self._done = True
37+
self._schedule_callbacks()
38+
39+
def add_done_callback(self, callback: Callable[[Future[T]], None]) -> None:
40+
self._callbacks.append(callback)
41+
if self._done:
42+
self._schedule_callbacks()
43+
44+
def _schedule_callbacks(self) -> None:
45+
for callback in self._callbacks:
46+
callback(self)
47+
48+
def __await__(self) -> Generator[Future[T], None, T]:
49+
if not self._done:
50+
yield self
51+
return self.result()
52+
53+
__iter__ = __await__

src/concurrency/patterns/join.py

Whitespace-only changes.

src/concurrency/patterns/lock.py

Whitespace-only changes.

src/concurrency/patterns/process_pool.py

Whitespace-only changes.

0 commit comments

Comments
 (0)