Skip to content

Commit 2f25886

Browse files
committed
test_subscribe: Replace EventEmitter with SimplePubSub
Replicates graphql/graphql-js@156c76e
1 parent c602d00 commit 2f25886

15 files changed

+330
-313
lines changed

.coveragerc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ omit =
1010
[report]
1111
exclude_lines =
1212
pragma: no cover
13+
except ImportError:
14+
\# Python <
1315
raise NotImplementedError
1416
raise TypeError\(f?"Unexpected
1517
assert False,

docs/conf.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@
108108
'execution': ['execute', 'middleware'],
109109
'language': ['ast', 'directive_locations', 'location',
110110
'source', 'token_kind', 'visitor'],
111-
'pyutils': ['event_emitter', 'frozen_list', 'path'],
111+
'pyutils': ['simple_pub_sub', 'frozen_list', 'path'],
112112
'subscription': [],
113113
'type': ['definition', 'directives', 'schema'],
114114
'utilities': ['find_breaking_changes', 'type_info'],
@@ -129,7 +129,9 @@
129129
ignore_references = set('''
130130
GNT GT T
131131
enum.Enum
132+
traceback
132133
asyncio.events.AbstractEventLoop
134+
graphql.subscription.map_async_iterator.MapAsyncIterator
133135
graphql.type.schema.InterfaceImplementations
134136
graphql.validation.validation_context.VariableUsage
135137
graphql.validation.rules.known_argument_names.KnownArgumentNamesOnDirectivesRule

docs/modules/pyutils.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ PyUtils
1313
.. autofunction:: did_you_mean
1414
.. autofunction:: register_description
1515
.. autofunction:: unregister_description
16-
.. autoclass:: EventEmitter
17-
.. autoclass:: EventEmitterAsyncIterator
16+
.. autoclass:: SimplePubSub
17+
.. autoclass:: SimplePubSubIterator
1818
.. autofunction:: identity_func
1919
.. autofunction:: inspect
2020
.. autofunction:: is_finite

docs/modules/subscription.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,14 @@ Subscription
44
.. currentmodule:: graphql.subscription
55

66
.. automodule:: graphql.subscription
7+
:no-members:
8+
:no-inherited-members:
79

10+
.. autofunction:: subscribe
11+
12+
Helpers
13+
-------
14+
15+
.. autofunction:: create_source_event_stream
16+
17+
.. autoclass:: MapAsyncIterator

src/graphql/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@
274274
MiddlewareManager,
275275
)
276276

277-
from .subscription import subscribe, create_source_event_stream
277+
from .subscription import subscribe, create_source_event_stream, MapAsyncIterator
278278

279279

280280
# Validate GraphQL queries.
@@ -622,6 +622,7 @@
622622
"MiddlewareManager",
623623
"subscribe",
624624
"create_source_event_stream",
625+
"MapAsyncIterator",
625626
"validate",
626627
"ValidationContext",
627628
"ValidationRule",

src/graphql/pyutils/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
unregister_description,
1818
)
1919
from .did_you_mean import did_you_mean
20-
from .event_emitter import EventEmitter, EventEmitterAsyncIterator
2120
from .identity_func import identity_func
2221
from .inspect import inspect
2322
from .is_awaitable import is_awaitable
@@ -31,6 +30,7 @@
3130
from .frozen_dict import FrozenDict
3231
from .path import Path
3332
from .print_path_list import print_path_list
33+
from .simple_pub_sub import SimplePubSub, SimplePubSubIterator
3434
from .undefined import Undefined, UndefinedType
3535

3636
__all__ = [
@@ -42,8 +42,6 @@
4242
"is_description",
4343
"register_description",
4444
"unregister_description",
45-
"EventEmitter",
46-
"EventEmitterAsyncIterator",
4745
"identity_func",
4846
"inspect",
4947
"is_awaitable",
@@ -57,6 +55,8 @@
5755
"FrozenDict",
5856
"Path",
5957
"print_path_list",
58+
"SimplePubSub",
59+
"SimplePubSubIterator",
6060
"Undefined",
6161
"UndefinedType",
6262
]

src/graphql/pyutils/event_emitter.py

Lines changed: 0 additions & 64 deletions
This file was deleted.

src/graphql/pyutils/simple_pub_sub.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
from asyncio import Future, Queue, ensure_future, sleep
2+
from inspect import isawaitable
3+
from typing import Any, AsyncIterator, Callable, Optional, Set
4+
5+
try:
6+
from asyncio import get_running_loop
7+
except ImportError:
8+
from asyncio import get_event_loop as get_running_loop # Python < 3.7
9+
10+
11+
__all__ = ["SimplePubSub", "SimplePubSubIterator"]
12+
13+
14+
class SimplePubSub:
15+
"""A very simple publish-subscript system.
16+
17+
Creates an AsyncIterator from an EventEmitter.
18+
19+
Useful for mocking a PubSub system for tests.
20+
"""
21+
22+
subscribers: Set[Callable]
23+
24+
def __init__(self) -> None:
25+
self.subscribers = set()
26+
27+
def emit(self, event: Any) -> bool:
28+
"""Emit an event."""
29+
for subscriber in self.subscribers:
30+
result = subscriber(event)
31+
if isawaitable(result):
32+
ensure_future(result)
33+
return bool(self.subscribers)
34+
35+
def get_subscriber(
36+
self, transform: Optional[Callable] = None
37+
) -> "SimplePubSubIterator":
38+
return SimplePubSubIterator(self, transform)
39+
40+
41+
class SimplePubSubIterator(AsyncIterator):
42+
def __init__(self, pubsub: SimplePubSub, transform: Optional[Callable]) -> None:
43+
self.pubsub = pubsub
44+
self.transform = transform
45+
self.pull_queue: Queue[Future] = Queue()
46+
self.push_queue: Queue[Any] = Queue()
47+
self.listening = True
48+
pubsub.subscribers.add(self.push_value)
49+
50+
def __aiter__(self) -> "SimplePubSubIterator":
51+
return self
52+
53+
async def __anext__(self) -> Any:
54+
if not self.listening:
55+
raise StopAsyncIteration
56+
await sleep(0)
57+
if not self.push_queue.empty():
58+
return await self.push_queue.get()
59+
future = get_running_loop().create_future()
60+
await self.pull_queue.put(future)
61+
return future
62+
63+
async def aclose(self) -> None:
64+
if self.listening:
65+
await self.empty_queue()
66+
67+
async def empty_queue(self) -> None:
68+
self.listening = False
69+
self.pubsub.subscribers.remove(self.push_value)
70+
while not self.pull_queue.empty():
71+
future = await self.pull_queue.get()
72+
future.cancel()
73+
while not self.push_queue.empty():
74+
await self.push_queue.get()
75+
76+
async def push_value(self, event: Any) -> None:
77+
value = event if self.transform is None else self.transform(event)
78+
if self.pull_queue.empty():
79+
await self.push_queue.put(value)
80+
else:
81+
(await self.pull_queue.get()).set_result(value)

src/graphql/subscription/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@
55
"""
66

77
from .subscribe import subscribe, create_source_event_stream
8+
from .map_async_iterator import MapAsyncIterator
89

9-
__all__ = ["subscribe", "create_source_event_stream"]
10+
__all__ = ["subscribe", "create_source_event_stream", "MapAsyncIterator"]

src/graphql/subscription/map_async_iterator.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ def __init__(
3030
self._close_event = Event()
3131

3232
def __aiter__(self) -> "MapAsyncIterator":
33+
"""Get the iterator object."""
3334
return self
3435

3536
async def __anext__(self) -> Any:
37+
"""Get the next value of the iterator."""
3638
if self.is_closed:
3739
if not isasyncgen(self.iterator):
3840
raise StopAsyncIteration
@@ -79,6 +81,7 @@ async def athrow(
7981
value: Optional[BaseException] = None,
8082
traceback: Optional[TracebackType] = None,
8183
) -> None:
84+
"""Throw an exception into the asynchronous iterator."""
8285
if not self.is_closed:
8386
athrow = getattr(self.iterator, "athrow", None)
8487
if athrow:
@@ -98,6 +101,7 @@ async def athrow(
98101
raise value
99102

100103
async def aclose(self) -> None:
104+
"""Close the iterator."""
101105
if not self.is_closed:
102106
aclose = getattr(self.iterator, "aclose", None)
103107
if aclose:
@@ -109,10 +113,12 @@ async def aclose(self) -> None:
109113

110114
@property
111115
def is_closed(self) -> bool:
116+
"""Check whether the iterator is closed."""
112117
return self._close_event.is_set()
113118

114119
@is_closed.setter
115120
def is_closed(self, value: bool) -> None:
121+
"""Mark the iterator as closed."""
116122
if value:
117123
self._close_event.set()
118124
else:

0 commit comments

Comments
 (0)