From 2abdbc70c2637da33f35af69abc8cd559c0b05f7 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Sat, 11 Feb 2017 23:34:19 -0500 Subject: [PATCH] Implement utilities for AsyncIterator. Closes #473. --- discord/abc.py | 2 +- discord/iterators.py | 145 ++++++++++++++++++++++++++++++------------- discord/reaction.py | 2 +- docs/api.rst | 84 ++++++++++++++++++++++--- 4 files changed, 181 insertions(+), 52 deletions(-) diff --git a/discord/abc.py b/discord/abc.py index 160cbb4b8..0398da6ff 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -740,7 +740,7 @@ class Messageable(metaclass=abc.ABCMeta): return [state.create_message(channel=channel, data=m) for m in data] def history(self, *, limit=100, before=None, after=None, around=None, reverse=None): - """Return an async iterator that enables receiving the destination's message history. + """Return an :class:`AsyncIterator` that enables receiving the destination's message history. You must have Read Message History permissions to use this. diff --git a/discord/iterators.py b/discord/iterators.py index 5aa780898..31d72569a 100644 --- a/discord/iterators.py +++ b/discord/iterators.py @@ -35,7 +35,108 @@ from .object import Object PY35 = sys.version_info >= (3, 5) -class ReactionIterator: +@asyncio.coroutine +def _probably_coroutine(f, e): + if asyncio.iscoroutinefunction(f): + return (yield from f(e)) + else: + return f(e) + +class _AsyncIterator: + __slots__ = () + + def get(self, **attrs): + def predicate(elem): + for attr, val in attrs.items(): + nested = attr.split('__') + obj = elem + for attribute in nested: + obj = getattr(obj, attribute) + + if obj != val: + return False + return True + + return self.find(predicate) + + @asyncio.coroutine + def find(self, predicate): + while True: + try: + elem = yield from self.get() + except NoMoreItems: + return None + + ret = yield from _probably_coroutine(predicate, elem) + if ret: + return elem + + def map(self, func): + return _MappedAsyncIterator(self, func) + + def filter(self, predicate): + return _FilteredAsyncIterator(self, predicate) + + @asyncio.coroutine + def flatten(self): + ret = [] + while True: + try: + item = yield from self.get() + except NoMoreItems: + return ret + else: + ret.append(item) + + if PY35: + @asyncio.coroutine + def __aiter__(self): + return self + + @asyncio.coroutine + def __anext__(self): + try: + msg = yield from self.get() + except NoMoreItems: + raise StopAsyncIteration() + else: + return msg + +def _identity(x): + return x + +class _MappedAsyncIterator(_AsyncIterator): + def __init__(self, iterator, func): + self.iterator = iterator + self.func = func + + @asyncio.coroutine + def get(self): + # this raises NoMoreItems and will propagate appropriately + item = yield from self.iterator.get() + return (yield from _probably_coroutine(self.func, item)) + +class _FilteredAsyncIterator(_AsyncIterator): + def __init__(self, iterator, predicate): + self.iterator = iterator + + if predicate is None: + predicate = _identity + + self.predicate = predicate + + @asyncio.coroutine + def get(self): + getter = self.iterator.get + pred = self.predicate + while True: + # propagate NoMoreItems similar to _MappedAsyncIterator + item = yield from getter() + ret = yield from _probably_coroutine(pred, item) + if ret: + return item + +class ReactionIterator(_AsyncIterator): def __init__(self, message, emoji, limit=100, after=None): self.message = message self.limit = limit @@ -85,32 +186,7 @@ class ReactionIterator: else: yield from self.users.put(User(state=self.state, data=element)) - @asyncio.coroutine - def flatten(self): - ret = [] - while True: - try: - user = yield from self.get() - except NoMoreItems: - return ret - else: - ret.append(user) - - if PY35: - @asyncio.coroutine - def __aiter__(self): - return self - - @asyncio.coroutine - def __anext__(self): - try: - msg = yield from self.get() - except NoMoreItems: - raise StopAsyncIteration() - else: - return msg - -class HistoryIterator: +class HistoryIterator(_AsyncIterator): """Iterator for receiving a channel's message history. The messages endpoint has two behaviours we care about here: @@ -281,18 +357,3 @@ class HistoryIterator: self.around = None return data return [] - - if PY35: - @asyncio.coroutine - def __aiter__(self): - return self - - @asyncio.coroutine - def __anext__(self): - try: - msg = yield from self.get() - return msg - except NoMoreItems: - # if we're still empty at this point... - # we didn't get any new messages so stop looping - raise StopAsyncIteration() diff --git a/discord/reaction.py b/discord/reaction.py index b5533776d..1454ca8e6 100644 --- a/discord/reaction.py +++ b/discord/reaction.py @@ -89,7 +89,7 @@ class Reaction: def users(self, limit=None, after=None): """|coro| - Returns an asynchronous iterator representing the + Returns an :class:`AsyncIterator` representing the users that have reacted to the message. The ``after`` parameter must represent a member diff --git a/docs/api.rst b/docs/api.rst index 87d31759c..6da9c1ed5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -40,14 +40,6 @@ Client .. autoclass:: AutoShardedClient :members: - -Voice ------ - -.. autoclass:: VoiceClient - :members: - - Opus Library ~~~~~~~~~~~~~ @@ -683,6 +675,82 @@ All enumerations are subclasses of `enum`_. You have sent a friend request to this user. +Async Iterator +---------------- + +Some API functions return an "async iterator". An async iterator is something that is +capable of being used in an `async for `_ +statement. + +These async iterators can be used as follows in 3.5 or higher: :: + + async for elem in channel.history(): + # do stuff with elem here + +If you are using 3.4 however, you will have to use the more verbose way: :: + + iterator = channel.history() # or whatever returns an async iterator + while True: + try: + item = yield from iterator.get() + except discord.NoMoreItems: + break + + # do stuff with item here + +Certain utilities make working with async iterators easier, detailed below. + +.. class:: AsyncIterator + + Represents the "AsyncIterator" concept. Note that no such class exists, + it is purely abstract. + + .. method:: get(**attrs) + + |coro| + + Similar to :func:`utils.get` except run over the async iterator. + + .. method:: find(predicate) + + |coro| + + Similar to :func:`utils.find` except run over the async iterator. + + Unlike :func:`utils.find`\, the predicate provided can be a + coroutine. + + :param predicate: The predicate to use. Can be a coroutine. + :return: The first element that returns ``True`` for the predicate or ``None``. + + .. method:: flatten() + + |coro| + + Flattens the async iterator into a ``list`` with all the elements. + + :return: A list of every element in the async iterator. + :rtype: list + + .. method:: map(func) + + This is similar to the built-in ``map`` function. Another + :class:`AsyncIterator` is returned that executes the function on + every element it is iterating over. This function can either be a + regular function or a coroutine. + + :param func: The function to call on every element. Could be a coroutine. + :return: An async iterator. + + .. method:: filter(predicate) + + This is similar to the built-in ``filter`` function. Another + :class:`AsyncIterator` is returned that filters over the original + async iterator. This predicate can be a regular function or a coroutine. + + :param predicate: The predicate to call on every element. Could be a coroutine. + :return: An async iterator. + .. _discord_api_data: Data Classes