diff --git a/README.rst b/README.rst index e703d124d..5ed738c82 100644 --- a/README.rst +++ b/README.rst @@ -87,14 +87,12 @@ Quick Example client = MyClient() client.run('token') -Note that in Python 3.4 you use ``@asyncio.coroutine`` instead of ``async def`` and ``yield from`` instead of ``await``. - You can find examples in the examples directory. Requirements ------------ -* Python 3.4.2+ +* Python 3.5.2+ * ``aiohttp`` library * ``websockets`` library * ``PyNaCl`` library (optional, for voice only) diff --git a/discord/abc.py b/discord/abc.py index 3f88491cb..597023943 100644 --- a/discord/abc.py +++ b/discord/abc.py @@ -188,8 +188,7 @@ class GuildChannel: def __str__(self): return self.name - @asyncio.coroutine - def _move(self, position, parent_id=None, lock_permissions=False, *, reason): + async def _move(self, position, parent_id=None, lock_permissions=False, *, reason): if position < 0: raise InvalidArgument('Channel position cannot be less than 0.') @@ -219,13 +218,12 @@ class GuildChannel: d.update(parent_id=parent_id, lock_permissions=lock_permissions) payload.append(d) - yield from http.bulk_channel_update(self.guild.id, payload, reason=reason) + await http.bulk_channel_update(self.guild.id, payload, reason=reason) self.position = position if parent_id is not _undefined: self.category_id = int(parent_id) if parent_id else None - @asyncio.coroutine - def _edit(self, options, reason): + async def _edit(self, options, reason): try: parent = options.pop('category') except KeyError: @@ -249,10 +247,10 @@ class GuildChannel: category = self.guild.get_channel(self.category_id) options['permission_overwrites'] = [c._asdict() for c in category._overwrites] else: - yield from self._move(position, parent_id=parent_id, lock_permissions=lock_permissions, reason=reason) + await self._move(position, parent_id=parent_id, lock_permissions=lock_permissions, reason=reason) if options: - data = yield from self._state.http.edit_channel(self.id, reason=reason, **options) + data = await self._state.http.edit_channel(self.id, reason=reason, **options) self._update(self.guild, data) def _fill_overwrites(self, data): @@ -466,8 +464,7 @@ class GuildChannel: return base - @asyncio.coroutine - def delete(self, *, reason=None): + async def delete(self, *, reason=None): """|coro| Deletes the channel. @@ -489,10 +486,9 @@ class GuildChannel: HTTPException Deleting the channel failed. """ - yield from self._state.http.delete_channel(self.id, reason=reason) + await self._state.http.delete_channel(self.id, reason=reason) - @asyncio.coroutine - def set_permissions(self, target, *, overwrite=_undefined, reason=None, **permissions): + async def set_permissions(self, target, *, overwrite=_undefined, reason=None, **permissions): """|coro| Sets the channel specific permission overwrites for a target in the @@ -579,15 +575,14 @@ class GuildChannel: # TODO: wait for event if overwrite is None: - yield from http.delete_channel_permissions(self.id, target.id, reason=reason) + await http.delete_channel_permissions(self.id, target.id, reason=reason) elif isinstance(overwrite, PermissionOverwrite): (allow, deny) = overwrite.pair() - yield from http.edit_channel_permissions(self.id, target.id, allow.value, deny.value, perm_type, reason=reason) + await http.edit_channel_permissions(self.id, target.id, allow.value, deny.value, perm_type, reason=reason) else: raise InvalidArgument('Invalid overwrite type provided.') - @asyncio.coroutine - def create_invite(self, *, reason=None, **fields): + async def create_invite(self, *, reason=None, **fields): """|coro| Creates an instant invite. @@ -624,11 +619,10 @@ class GuildChannel: The invite that was created. """ - data = yield from self._state.http.create_invite(self.id, reason=reason, **fields) + data = await self._state.http.create_invite(self.id, reason=reason, **fields) return Invite.from_incomplete(data=data, state=self._state) - @asyncio.coroutine - def invites(self): + async def invites(self): """|coro| Returns a list of all active instant invites from this channel. @@ -649,7 +643,7 @@ class GuildChannel: """ state = self._state - data = yield from state.http.invites_from_channel(self.id) + data = await state.http.invites_from_channel(self.id) result = [] for invite in data: @@ -676,13 +670,11 @@ class Messageable(metaclass=abc.ABCMeta): __slots__ = () - @asyncio.coroutine @abc.abstractmethod - def _get_channel(self): + async def _get_channel(self): raise NotImplementedError - @asyncio.coroutine - def send(self, content=None, *, tts=False, embed=None, file=None, files=None, delete_after=None, nonce=None): + async def send(self, content=None, *, tts=False, embed=None, file=None, files=None, delete_after=None, nonce=None): """|coro| Sends a message to the destination with the content given. @@ -735,7 +727,7 @@ class Messageable(metaclass=abc.ABCMeta): The message that was sent. """ - channel = yield from self._get_channel() + channel = await self._get_channel() state = self._state content = str(content) if content is not None else None if embed is not None: @@ -749,7 +741,7 @@ class Messageable(metaclass=abc.ABCMeta): raise InvalidArgument('file parameter must be File') try: - data = yield from state.http.send_files(channel.id, files=[(file.open_file(), file.filename)], + data = await state.http.send_files(channel.id, files=[(file.open_file(), file.filename)], content=content, tts=tts, embed=embed, nonce=nonce) finally: file.close() @@ -760,28 +752,26 @@ class Messageable(metaclass=abc.ABCMeta): try: param = [(f.open_file(), f.filename) for f in files] - data = yield from state.http.send_files(channel.id, files=param, content=content, tts=tts, + data = await state.http.send_files(channel.id, files=param, content=content, tts=tts, embed=embed, nonce=nonce) finally: for f in files: f.close() else: - data = yield from state.http.send_message(channel.id, content, tts=tts, embed=embed, nonce=nonce) + data = await state.http.send_message(channel.id, content, tts=tts, embed=embed, nonce=nonce) ret = state.create_message(channel=channel, data=data) if delete_after is not None: - @asyncio.coroutine - def delete(): - yield from asyncio.sleep(delete_after, loop=state.loop) + async def delete(): + await asyncio.sleep(delete_after, loop=state.loop) try: - yield from ret.delete() + await ret.delete() except: pass - compat.create_task(delete(), loop=state.loop) + asyncio.ensure_future(delete(), loop=state.loop) return ret - @asyncio.coroutine - def trigger_typing(self): + async def trigger_typing(self): """|coro| Triggers a *typing* indicator to the destination. @@ -789,8 +779,8 @@ class Messageable(metaclass=abc.ABCMeta): *Typing* indicator will go away after 10 seconds, or after a message is sent. """ - channel = yield from self._get_channel() - yield from self._state.http.send_typing(channel.id) + channel = await self._get_channel() + await self._state.http.send_typing(channel.id) def typing(self): """Returns a context manager that allows you to type for an indefinite period of time. @@ -811,8 +801,7 @@ class Messageable(metaclass=abc.ABCMeta): """ return Typing(self) - @asyncio.coroutine - def get_message(self, id): + async def get_message(self, id): """|coro| Retrieves a single :class:`Message` from the destination. @@ -839,12 +828,11 @@ class Messageable(metaclass=abc.ABCMeta): Retrieving the message failed. """ - channel = yield from self._get_channel() - data = yield from self._state.http.get_message(channel.id, id) + channel = await self._get_channel() + data = await self._state.http.get_message(channel.id, id) return self._state.create_message(channel=channel, data=data) - @asyncio.coroutine - def pins(self): + async def pins(self): """|coro| Returns a :class:`list` of :class:`Message` that are currently pinned. @@ -855,9 +843,9 @@ class Messageable(metaclass=abc.ABCMeta): Retrieving the pinned messages failed. """ - channel = yield from self._get_channel() + channel = await self._get_channel() state = self._state - data = yield from state.http.pins_from(channel.id) + data = await state.http.pins_from(channel.id) 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): @@ -916,19 +904,6 @@ class Messageable(metaclass=abc.ABCMeta): messages = await channel.history(limit=123).flatten() # messages is now a list of Message... - - Python 3.4 Usage :: - - count = 0 - iterator = channel.history(limit=200) - while True: - try: - message = yield from iterator.next() - except discord.NoMoreItems: - break - else: - if message.author == client.user: - counter += 1 """ return HistoryIterator(self, limit=limit, before=before, after=after, around=around, reverse=reverse) @@ -951,8 +926,7 @@ class Connectable(metaclass=abc.ABCMeta): def _get_voice_state_pair(self): raise NotImplementedError - @asyncio.coroutine - def connect(self, *, timeout=60.0, reconnect=True): + async def connect(self, *, timeout=60.0, reconnect=True): """|coro| Connects to voice and creates a :class:`VoiceClient` to establish @@ -991,10 +965,10 @@ class Connectable(metaclass=abc.ABCMeta): state._add_voice_client(key_id, voice) try: - yield from voice.connect(reconnect=reconnect) + await voice.connect(reconnect=reconnect) except asyncio.TimeoutError as e: try: - yield from voice.disconnect(force=True) + await voice.disconnect(force=True) except: # we don't care if disconnect failed because connection failed pass diff --git a/discord/channel.py b/discord/channel.py index ac5f33830..27b6301e0 100644 --- a/discord/channel.py +++ b/discord/channel.py @@ -37,10 +37,9 @@ import asyncio __all__ = ('TextChannel', 'VoiceChannel', 'DMChannel', 'CategoryChannel', 'GroupChannel', '_channel_factory') -@asyncio.coroutine -def _single_delete_strategy(messages): +async def _single_delete_strategy(messages): for m in messages: - yield from m.delete() + await m.delete() class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): """Represents a Discord guild text channel. @@ -100,8 +99,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): self.nsfw = data.get('nsfw', False) self._fill_overwrites(data) - @asyncio.coroutine - def _get_channel(self): + async def _get_channel(self): return self def permissions_for(self, member): @@ -124,8 +122,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): n = self.name return self.nsfw or n == 'nsfw' or n[:5] == 'nsfw-' - @asyncio.coroutine - def edit(self, *, reason=None, **options): + async def edit(self, *, reason=None, **options): """|coro| Edits the channel. @@ -161,10 +158,9 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): HTTPException Editing the channel failed. """ - yield from self._edit(options, reason=reason) + await self._edit(options, reason=reason) - @asyncio.coroutine - def delete_messages(self, messages): + async def delete_messages(self, messages): """|coro| Deletes a list of messages. This is similar to :meth:`Message.delete` @@ -205,17 +201,16 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): if len(messages) == 1: message_id = messages[0].id - yield from self._state.http.delete_message(self.id, message_id) + await self._state.http.delete_message(self.id, message_id) return if len(messages) > 100: raise ClientException('Can only bulk delete messages up to 100 messages') message_ids = [m.id for m in messages] - yield from self._state.http.delete_messages(self.id, message_ids) + await self._state.http.delete_messages(self.id, message_ids) - @asyncio.coroutine - def purge(self, *, limit=100, check=None, before=None, after=None, around=None, reverse=False, bulk=True): + async def purge(self, *, limit=100, check=None, before=None, after=None, around=None, reverse=False, bulk=True): """|coro| Purges a list of messages that meet the criteria given by the predicate @@ -289,34 +284,34 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): while True: try: - msg = yield from iterator.next() + msg = await iterator.next() except NoMoreItems: # no more messages to poll if count >= 2: # more than 2 messages -> bulk delete to_delete = ret[-count:] - yield from strategy(to_delete) + await strategy(to_delete) elif count == 1: # delete a single message - yield from ret[-1].delete() + await ret[-1].delete() return ret else: if count == 100: # we've reached a full 'queue' to_delete = ret[-100:] - yield from strategy(to_delete) + await strategy(to_delete) count = 0 - yield from asyncio.sleep(1) + await asyncio.sleep(1) if check(msg): if msg.id < minimum_time: # older than 14 days old if count == 1: - yield from ret[-1].delete() + await ret[-1].delete() elif count >= 2: to_delete = ret[-count:] - yield from strategy(to_delete) + await strategy(to_delete) count = 0 strategy = _single_delete_strategy @@ -324,8 +319,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): count += 1 ret.append(msg) - @asyncio.coroutine - def webhooks(self): + async def webhooks(self): """|coro| Gets the list of webhooks from this channel. @@ -343,11 +337,10 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): The webhooks for this channel. """ - data = yield from self._state.http.channel_webhooks(self.id) + data = await self._state.http.channel_webhooks(self.id) return [Webhook.from_state(d, state=self._state) for d in data] - @asyncio.coroutine - def create_webhook(self, *, name=None, avatar=None): + async def create_webhook(self, *, name=None, avatar=None): """|coro| Creates a webhook for this channel. @@ -381,7 +374,7 @@ class TextChannel(discord.abc.Messageable, discord.abc.GuildChannel, Hashable): if name is not None: name = str(name) - data = yield from self._state.http.create_webhook(self.id, name=name, avatar=avatar) + data = await self._state.http.create_webhook(self.id, name=name, avatar=avatar) return Webhook.from_state(data, state=self._state) class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable): @@ -461,8 +454,7 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable): ret.append(member) return ret - @asyncio.coroutine - def edit(self, *, reason=None, **options): + async def edit(self, *, reason=None, **options): """|coro| Edits the channel. @@ -497,7 +489,7 @@ class VoiceChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable): Editing the channel failed. """ - yield from self._edit(options, reason=reason) + await self._edit(options, reason=reason) class CategoryChannel(discord.abc.GuildChannel, Hashable): """Represents a Discord channel category. @@ -558,8 +550,7 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable): n = self.name return self.nsfw or n == 'nsfw' or n[:5] == 'nsfw-' - @asyncio.coroutine - def edit(self, *, reason=None, **options): + async def edit(self, *, reason=None, **options): """|coro| Edits the channel. @@ -593,11 +584,11 @@ class CategoryChannel(discord.abc.GuildChannel, Hashable): except KeyError: pass else: - yield from self._move(position, reason=reason) + await self._move(position, reason=reason) self.position = position if options: - data = yield from self._state.http.edit_channel(self.id, reason=reason, **options) + data = await self._state.http.edit_channel(self.id, reason=reason, **options) self._update(self.guild, data) @property @@ -652,8 +643,7 @@ class DMChannel(discord.abc.Messageable, Hashable): self.me = me self.id = int(data['id']) - @asyncio.coroutine - def _get_channel(self): + async def _get_channel(self): return self def __str__(self): @@ -756,8 +746,7 @@ class GroupChannel(discord.abc.Messageable, Hashable): else: self.owner = utils.find(lambda u: u.id == owner_id, self.recipients) - @asyncio.coroutine - def _get_channel(self): + async def _get_channel(self): return self def __str__(self): @@ -820,8 +809,7 @@ class GroupChannel(discord.abc.Messageable, Hashable): return base - @asyncio.coroutine - def add_recipients(self, *recipients): + async def add_recipients(self, *recipients): """|coro| Adds recipients to this group. @@ -846,10 +834,9 @@ class GroupChannel(discord.abc.Messageable, Hashable): req = self._state.http.add_group_recipient for recipient in recipients: - yield from req(self.id, recipient.id) + await req(self.id, recipient.id) - @asyncio.coroutine - def remove_recipients(self, *recipients): + async def remove_recipients(self, *recipients): """|coro| Removes recipients from this group. @@ -869,10 +856,9 @@ class GroupChannel(discord.abc.Messageable, Hashable): req = self._state.http.remove_group_recipient for recipient in recipients: - yield from req(self.id, recipient.id) + await req(self.id, recipient.id) - @asyncio.coroutine - def edit(self, **fields): + async def edit(self, **fields): """|coro| Edits the group. @@ -900,11 +886,10 @@ class GroupChannel(discord.abc.Messageable, Hashable): if icon_bytes is not None: fields['icon'] = utils._bytes_to_base64_data(icon_bytes) - data = yield from self._state.http.edit_group(self.id, **fields) + data = await self._state.http.edit_group(self.id, **fields) self._update_group(data) - @asyncio.coroutine - def leave(self): + async def leave(self): """|coro| Leave the group. @@ -917,7 +902,7 @@ class GroupChannel(discord.abc.Messageable, Hashable): Leaving the group failed. """ - yield from self._state.http.leave_group(self.id) + await self._state.http.leave_group(self.id) def _channel_factory(channel_type): value = try_enum(ChannelType, channel_type) diff --git a/discord/client.py b/discord/client.py index 0434d1e2f..a46f16376 100644 --- a/discord/client.py +++ b/discord/client.py @@ -48,7 +48,6 @@ import sys, re import signal from collections import namedtuple -PY35 = sys.version_info >= (3, 5) log = logging.getLogger(__name__) AppInfo = namedtuple('AppInfo', 'id name description icon owner') @@ -139,12 +138,10 @@ class Client: # internals - @asyncio.coroutine - def _syncer(self, guilds): - yield from self.ws.request_sync(guilds) + async def _syncer(self, guilds): + await self.ws.request_sync(guilds) - @asyncio.coroutine - def _chunker(self, guild): + async def _chunker(self, guild): try: guild_id = guild.id except AttributeError: @@ -159,7 +156,7 @@ class Client: } } - yield from self.ws.send_as_json(payload) + await self.ws.send_as_json(payload) def handle_ready(self): self._ready.set() @@ -218,15 +215,14 @@ class Client: """:obj:`bool`: Specifies if the client's internal cache is ready for use.""" return self._ready.is_set() - @asyncio.coroutine - def _run_event(self, coro, event_name, *args, **kwargs): + async def _run_event(self, coro, event_name, *args, **kwargs): try: - yield from coro(*args, **kwargs) + await coro(*args, **kwargs) except asyncio.CancelledError: pass except Exception: try: - yield from self.on_error(event_name, *args, **kwargs) + await self.on_error(event_name, *args, **kwargs) except asyncio.CancelledError: pass @@ -276,10 +272,9 @@ class Client: except AttributeError: pass else: - compat.create_task(self._run_event(coro, method, *args, **kwargs), loop=self.loop) + asyncio.ensure_future(self._run_event(coro, method, *args, **kwargs), loop=self.loop) - @asyncio.coroutine - def on_error(self, event_method, *args, **kwargs): + async def on_error(self, event_method, *args, **kwargs): """|coro| The default error handler provided by the client. @@ -291,8 +286,7 @@ class Client: print('Ignoring exception in {}'.format(event_method), file=sys.stderr) traceback.print_exc() - @asyncio.coroutine - def request_offline_members(self, *guilds): + async def request_offline_members(self, *guilds): """|coro| Requests previously offline members from the guild to be filled up @@ -318,12 +312,11 @@ class Client: if any(not g.large or g.unavailable for g in guilds): raise InvalidArgument('An unavailable or non-large guild was passed.') - yield from self._connection.request_offline_members(guilds) + await self._connection.request_offline_members(guilds) # login state management - @asyncio.coroutine - def login(self, token, *, bot=True): + async def login(self, token, *, bot=True): """|coro| Logs in the client with the specified credentials. @@ -350,34 +343,31 @@ class Client: """ log.info('logging in using static token') - yield from self.http.static_login(token, bot=bot) + await self.http.static_login(token, bot=bot) self._connection.is_bot = bot - @asyncio.coroutine - def logout(self): + async def logout(self): """|coro| Logs out of Discord and closes all connections. """ - yield from self.close() + await self.close() - @asyncio.coroutine - def _connect(self): + async def _connect(self): coro = DiscordWebSocket.from_client(self, shard_id=self.shard_id) - self.ws = yield from asyncio.wait_for(coro, timeout=180.0, loop=self.loop) + self.ws = await asyncio.wait_for(coro, timeout=180.0, loop=self.loop) while True: try: - yield from self.ws.poll_event() + await self.ws.poll_event() except ResumeWebSocket as e: log.info('Got a request to RESUME the websocket.') coro = DiscordWebSocket.from_client(self, shard_id=self.shard_id, session=self.ws.session_id, sequence=self.ws.sequence, resume=True) - self.ws = yield from asyncio.wait_for(coro, timeout=180.0, loop=self.loop) + self.ws = await asyncio.wait_for(coro, timeout=180.0, loop=self.loop) - @asyncio.coroutine - def connect(self, *, reconnect=True): + async def connect(self, *, reconnect=True): """|coro| Creates a websocket connection and lets the websocket listen @@ -405,7 +395,7 @@ class Client: backoff = ExponentialBackoff() while not self.is_closed(): try: - yield from self._connect() + await self._connect() except (OSError, HTTPException, GatewayNotFound, @@ -416,7 +406,7 @@ class Client: websockets.WebSocketProtocolError) as e: if not reconnect: - yield from self.close() + await self.close() if isinstance(e, ConnectionClosed) and e.code == 1000: # clean close, don't re-raise this return @@ -431,15 +421,14 @@ class Client: # regardless and rely on is_closed instead if isinstance(e, ConnectionClosed): if e.code != 1000: - yield from self.close() + await self.close() raise retry = backoff.delay() log.exception("Attempting a reconnect in %.2fs", retry) - yield from asyncio.sleep(retry, loop=self.loop) + await asyncio.sleep(retry, loop=self.loop) - @asyncio.coroutine - def close(self): + async def close(self): """|coro| Closes the connection to discord. @@ -451,16 +440,16 @@ class Client: for voice in self.voice_clients: try: - yield from voice.disconnect() + await voice.disconnect() except: # if an error happens during disconnects, disregard it. pass if self.ws is not None and self.ws.open: - yield from self.ws.close() + await self.ws.close() - yield from self.http.close() + await self.http.close() self._ready.clear() def clear(self): @@ -475,8 +464,7 @@ class Client: self._connection.clear() self.http.recreate() - @asyncio.coroutine - def start(self, *args, **kwargs): + async def start(self, *args, **kwargs): """|coro| A shorthand coroutine for :meth:`login` + :meth:`connect`. @@ -484,8 +472,8 @@ class Client: bot = kwargs.pop('bot', True) reconnect = kwargs.pop('reconnect', True) - yield from self.login(*args, bot=bot) - yield from self.connect(reconnect=reconnect) + await self.login(*args, bot=bot) + await self.connect(reconnect=reconnect) def _do_cleanup(self): log.info('Cleaning up event loop.') @@ -493,7 +481,7 @@ class Client: if loop.is_closed(): return # we're already cleaning up - task = compat.create_task(self.close(), loop=loop) + task = asyncio.ensure_future(self.close(), loop=loop) def _silence_gathered(fut): try: @@ -558,7 +546,7 @@ class Client: loop.add_signal_handler(signal.SIGINT, self._do_cleanup) loop.add_signal_handler(signal.SIGTERM, self._do_cleanup) - task = compat.create_task(self.start(*args, **kwargs), loop=loop) + task = asyncio.ensure_future(self.start(*args, **kwargs), loop=loop) def stop_loop_on_finish(fut): loop.stop() @@ -661,13 +649,12 @@ class Client: # listeners/waiters - @asyncio.coroutine - def wait_until_ready(self): + async def wait_until_ready(self): """|coro| Waits until the client's internal cache is all ready. """ - yield from self._ready.wait() + await self._ready.wait() def wait_for(self, event, *, check=None, timeout=None): """|coro| @@ -751,7 +738,7 @@ class Client: :ref:`event reference `. """ - future = compat.create_future(self.loop) + future = self.loop.create_future() if check is None: def _check(*args): return True @@ -782,8 +769,7 @@ class Client: Using the basic :meth:`event` decorator: :: @client.event - @asyncio.coroutine - def on_ready(): + async def on_ready(): print('Ready!') Saving characters by using the :meth:`async_event` decorator: :: @@ -808,8 +794,7 @@ class Client: return self.event(coro) - @asyncio.coroutine - def change_presence(self, *, activity=None, status=None, afk=False): + async def change_presence(self, *, activity=None, status=None, afk=False): """|coro| Changes the client's presence. @@ -851,7 +836,7 @@ class Client: status_enum = status status = str(status) - yield from self.ws.change_presence(activity=activity, status=status, afk=afk) + await self.ws.change_presence(activity=activity, status=status, afk=afk) for guild in self._connection.guilds: me = guild.me @@ -863,8 +848,7 @@ class Client: # Guild stuff - @asyncio.coroutine - def create_guild(self, name, region=None, icon=None): + async def create_guild(self, name, region=None, icon=None): """|coro| Creates a :class:`Guild`. @@ -903,13 +887,12 @@ class Client: else: region = region.value - data = yield from self.http.create_guild(name, region, icon) + data = await self.http.create_guild(name, region, icon) return Guild(data=data, state=self._connection) # Invite management - @asyncio.coroutine - def get_invite(self, url): + async def get_invite(self, url): """|coro| Gets an :class:`Invite` from a discord.gg URL or ID. @@ -939,11 +922,10 @@ class Client: """ invite_id = self._resolve_invite(url) - data = yield from self.http.get_invite(invite_id) + data = await self.http.get_invite(invite_id) return Invite.from_incomplete(state=self._connection, data=data) - @asyncio.coroutine - def delete_invite(self, invite): + async def delete_invite(self, invite): """|coro| Revokes an :class:`Invite`, URL, or ID to an invite. @@ -967,12 +949,11 @@ class Client: """ invite_id = self._resolve_invite(invite) - yield from self.http.delete_invite(invite_id) + await self.http.delete_invite(invite_id) # Miscellaneous stuff - @asyncio.coroutine - def application_info(self): + async def application_info(self): """|coro| Retrieve's the bot's application information. @@ -987,13 +968,12 @@ class Client: HTTPException Retrieving the information failed somehow. """ - data = yield from self.http.application_info() + data = await self.http.application_info() return AppInfo(id=int(data['id']), name=data['name'], description=data['description'], icon=data['icon'], owner=User(state=self._connection, data=data['owner'])) - @asyncio.coroutine - def get_user_info(self, user_id): + async def get_user_info(self, user_id): """|coro| Retrieves a :class:`User` based on their ID. This can only @@ -1018,11 +998,10 @@ class Client: HTTPException Fetching the user failed. """ - data = yield from self.http.get_user_info(user_id) + data = await self.http.get_user_info(user_id) return User(state=self._connection, data=data) - @asyncio.coroutine - def get_user_profile(self, user_id): + async def get_user_profile(self, user_id): """|coro| Gets an arbitrary user's profile. This can only be used by non-bot accounts. @@ -1046,7 +1025,7 @@ class Client: """ state = self._connection - data = yield from self.http.get_user_profile(user_id) + data = await self.http.get_user_profile(user_id) def transform(d): return state._get_guild(int(d['id'])) @@ -1060,8 +1039,7 @@ class Client: user=User(data=user, state=state), connected_accounts=data['connected_accounts']) - @asyncio.coroutine - def get_webhook_info(self, webhook_id): + async def get_webhook_info(self, webhook_id): """|coro| Retrieves a :class:`Webhook` with the specified ID. @@ -1080,5 +1058,5 @@ class Client: :class:`Webhook` The webhook you requested. """ - data = yield from self.http.get_webhook(webhook_id) + data = await self.http.get_webhook(webhook_id) return Webhook.from_state(data, state=self._connection) diff --git a/discord/compat.py b/discord/compat.py deleted file mode 100644 index 9c51e7847..000000000 --- a/discord/compat.py +++ /dev/null @@ -1,140 +0,0 @@ -# -*- coding: utf-8 -*- -""" -The MIT License (MIT) - -Copyright (c) 2015-2017 Rapptz - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. -""" - -import concurrent.futures -import asyncio - -try: - create_task = asyncio.ensure_future -except AttributeError: - create_task = getattr(asyncio, 'async') - -try: - _create_future = asyncio.AbstractEventLoop.create_future -except AttributeError: - def create_future(loop): - return asyncio.Future(loop=loop) -else: - def create_future(loop): - return loop.create_future() - -try: - run_coroutine_threadsafe = asyncio.run_coroutine_threadsafe -except AttributeError: - # the following code is slightly modified from the - # official asyncio repository that could be found here: - # https://github.com/python/asyncio/blob/master/asyncio/futures.py - # with a commit hash of 5c7efbcdfbe6a5c25b4cd5df22d9a15ab4062c8e - # this portion is licensed under Apache license 2.0 - - def _set_concurrent_future_state(concurrent, source): - """Copy state from a future to a concurrent.futures.Future.""" - assert source.done() - if source.cancelled(): - concurrent.cancel() - if not concurrent.set_running_or_notify_cancel(): - return - exception = source.exception() - if exception is not None: - concurrent.set_exception(exception) - else: - result = source.result() - concurrent.set_result(result) - - - def _copy_future_state(source, dest): - """Internal helper to copy state from another Future. - The other Future may be a concurrent.futures.Future. - """ - assert source.done() - if dest.cancelled(): - return - assert not dest.done() - if source.cancelled(): - dest.cancel() - else: - exception = source.exception() - if exception is not None: - dest.set_exception(exception) - else: - result = source.result() - dest.set_result(result) - - def _chain_future(source, destination): - """Chain two futures so that when one completes, so does the other. - The result (or exception) of source will be copied to destination. - If destination is cancelled, source gets cancelled too. - Compatible with both asyncio.Future and concurrent.futures.Future. - """ - if not isinstance(source, (asyncio.Future, concurrent.futures.Future)): - raise TypeError('A future is required for source argument') - - if not isinstance(destination, (asyncio.Future, concurrent.futures.Future)): - raise TypeError('A future is required for destination argument') - - source_loop = source._loop if isinstance(source, asyncio.Future) else None - dest_loop = destination._loop if isinstance(destination, asyncio.Future) else None - - def _set_state(future, other): - if isinstance(future, asyncio.Future): - _copy_future_state(other, future) - else: - _set_concurrent_future_state(future, other) - - def _call_check_cancel(destination): - if destination.cancelled(): - if source_loop is None or source_loop is dest_loop: - source.cancel() - else: - source_loop.call_soon_threadsafe(source.cancel) - - def _call_set_state(source): - if dest_loop is None or dest_loop is source_loop: - _set_state(destination, source) - else: - dest_loop.call_soon_threadsafe(_set_state, destination, source) - - destination.add_done_callback(_call_check_cancel) - source.add_done_callback(_call_set_state) - - def run_coroutine_threadsafe(coro, loop): - """Submit a coroutine object to a given event loop. - - Return a concurrent.futures.Future to access the result. - """ - if not asyncio.iscoroutine(coro): - raise TypeError('A coroutine object is required') - - future = concurrent.futures.Future() - - def callback(): - try: - _chain_future(create_task(coro, loop=loop), future) - except Exception as exc: - if future.set_running_or_notify_cancel(): - future.set_exception(exc) - raise - loop.call_soon_threadsafe(callback) - return future diff --git a/discord/context_managers.py b/discord/context_managers.py index 93d292c21..94f817d47 100644 --- a/discord/context_managers.py +++ b/discord/context_managers.py @@ -40,18 +40,17 @@ class Typing: self.loop = messageable._state.loop self.messageable = messageable - @asyncio.coroutine - def do_typing(self): + async def do_typing(self): try: channel = self._channel except AttributeError: - channel = yield from self.messageable._get_channel() + channel = await self.messageable._get_channel() typing = channel._state.http.send_typing while True: - yield from typing(channel.id) - yield from asyncio.sleep(5) + await typing(channel.id) + await asyncio.sleep(5) def __enter__(self): self.task = create_task(self.do_typing(), loop=self.loop) @@ -61,12 +60,10 @@ class Typing: def __exit__(self, exc_type, exc, tb): self.task.cancel() - @asyncio.coroutine - def __aenter__(self): - self._channel = channel = yield from self.messageable._get_channel() - yield from channel._state.http.send_typing(channel.id) + async def __aenter__(self): + self._channel = channel = await self.messageable._get_channel() + await channel._state.http.send_typing(channel.id) return self.__enter__() - @asyncio.coroutine - def __aexit__(self, exc_type, exc, tb): + async def __aexit__(self, exc_type, exc, tb): self.task.cancel() diff --git a/discord/emoji.py b/discord/emoji.py index 13eeb307e..21c11c717 100644 --- a/discord/emoji.py +++ b/discord/emoji.py @@ -203,8 +203,7 @@ class Emoji(Hashable): """:class:`Guild`: The guild this emoji belongs to.""" return self._state._get_guild(self.guild_id) - @asyncio.coroutine - def delete(self, *, reason=None): + async def delete(self, *, reason=None): """|coro| Deletes the custom emoji. @@ -227,10 +226,9 @@ class Emoji(Hashable): An error occurred deleting the emoji. """ - yield from self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason) + await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason) - @asyncio.coroutine - def edit(self, *, name, reason=None): + async def edit(self, *, name, reason=None): """|coro| Edits the custom emoji. @@ -255,4 +253,4 @@ class Emoji(Hashable): An error occurred editing the emoji. """ - yield from self._state.http.edit_custom_emoji(self.guild.id, self.id, name=name, reason=reason) + await self._state.http.edit_custom_emoji(self.guild.id, self.id, name=name, reason=reason) diff --git a/discord/errors.py b/discord/errors.py index 95e277d05..3111675ae 100644 --- a/discord/errors.py +++ b/discord/errors.py @@ -40,9 +40,7 @@ class ClientException(DiscordException): class NoMoreItems(DiscordException): """Exception that is thrown when an async iteration operation has no more - items. This is mainly exposed for Python 3.4 support where `StopAsyncIteration` - is not provided. - """ + items.""" pass class GatewayNotFound(DiscordException): diff --git a/discord/ext/commands/bot.py b/discord/ext/commands/bot.py index 9ef4143c7..f2b7a8067 100644 --- a/discord/ext/commands/bot.py +++ b/discord/ext/commands/bot.py @@ -91,8 +91,7 @@ _mention_pattern = re.compile('|'.join(_mentions_transforms.keys())) def _is_submodule(parent, child): return parent == child or child.startswith(parent + ".") -@asyncio.coroutine -def _default_help_command(ctx, *commands : str): +async def _default_help_command(ctx, *commands : str): """Shows this message.""" bot = ctx.bot destination = ctx.message.author if bot.pm_help else ctx.message.channel @@ -102,7 +101,7 @@ def _default_help_command(ctx, *commands : str): # help by itself just lists our own commands. if len(commands) == 0: - pages = yield from bot.formatter.format_help_for(ctx, bot) + pages = await bot.formatter.format_help_for(ctx, bot) elif len(commands) == 1: # try to see if it is a cog name name = _mention_pattern.sub(repl, commands[0]) @@ -112,15 +111,15 @@ def _default_help_command(ctx, *commands : str): else: command = bot.all_commands.get(name) if command is None: - yield from destination.send(bot.command_not_found.format(name)) + await destination.send(bot.command_not_found.format(name)) return - pages = yield from bot.formatter.format_help_for(ctx, command) + pages = await bot.formatter.format_help_for(ctx, command) else: name = _mention_pattern.sub(repl, commands[0]) command = bot.all_commands.get(name) if command is None: - yield from destination.send(bot.command_not_found.format(name)) + await destination.send(bot.command_not_found.format(name)) return for key in commands[1:]: @@ -128,13 +127,13 @@ def _default_help_command(ctx, *commands : str): key = _mention_pattern.sub(repl, key) command = command.all_commands.get(key) if command is None: - yield from destination.send(bot.command_not_found.format(key)) + await destination.send(bot.command_not_found.format(key)) return except AttributeError: - yield from destination.send(bot.command_has_no_subcommands.format(command, key)) + await destination.send(bot.command_has_no_subcommands.format(command, key)) return - pages = yield from bot.formatter.format_help_for(ctx, command) + pages = await bot.formatter.format_help_for(ctx, command) if bot.pm_help is None: characters = sum(map(lambda l: len(l), pages)) @@ -143,7 +142,7 @@ def _default_help_command(ctx, *commands : str): destination = ctx.message.author for page in pages: - yield from destination.send(page) + await destination.send(page) class BotBase(GroupMixin): def __init__(self, command_prefix, formatter=None, description=None, pm_help=False, **options): @@ -189,10 +188,9 @@ class BotBase(GroupMixin): ev = 'on_' + event_name for event in self.extra_events.get(ev, []): coro = self._run_event(event, event_name, *args, **kwargs) - discord.compat.create_task(coro, loop=self.loop) + asyncio.ensure_future(coro, loop=self.loop) - @asyncio.coroutine - def close(self): + async def close(self): for extension in tuple(self.extensions): try: self.unload_extension(extension) @@ -205,10 +203,9 @@ class BotBase(GroupMixin): except: pass - yield from super().close() + await super().close() - @asyncio.coroutine - def on_command_error(self, context, exception): + async def on_command_error(self, context, exception): """|coro| The default command error handler provided by the bot. @@ -335,17 +332,15 @@ class BotBase(GroupMixin): self.add_check(func, call_once=True) return func - @asyncio.coroutine - def can_run(self, ctx, *, call_once=False): + async def can_run(self, ctx, *, call_once=False): data = self._check_once if call_once else self._checks if len(data) == 0: return True - return (yield from discord.utils.async_all(f(ctx) for f in data)) + return (await discord.utils.async_all(f(ctx) for f in data)) - @asyncio.coroutine - def is_owner(self, user): + async def is_owner(self, user): """Checks if a :class:`.User` or :class:`.Member` is the owner of this bot. @@ -359,7 +354,7 @@ class BotBase(GroupMixin): """ if self.owner_id is None: - app = yield from self.application_info() + app = await self.application_info() self.owner_id = owner_id = app.owner.id return user.id == owner_id return user.id == self.owner_id @@ -773,8 +768,7 @@ class BotBase(GroupMixin): # command processing - @asyncio.coroutine - def get_prefix(self, message): + async def get_prefix(self, message): """|coro| Retrieves the prefix the bot is listening to @@ -801,9 +795,7 @@ class BotBase(GroupMixin): """ prefix = ret = self.command_prefix if callable(prefix): - ret = prefix(self, message) - if asyncio.iscoroutine(ret): - ret = yield from ret + ret = await discord.utils.maybe_coroutine(prefix, self, ret) if isinstance(ret, (list, tuple)): ret = [p for p in ret if p] @@ -813,8 +805,7 @@ class BotBase(GroupMixin): return ret - @asyncio.coroutine - def get_context(self, message, *, cls=Context): + async def get_context(self, message, *, cls=Context): """|coro| Returns the invocation context from the message. @@ -850,7 +841,7 @@ class BotBase(GroupMixin): if self._skip_check(message.author.id, self.user.id): return ctx - prefix = yield from self.get_prefix(message) + prefix = await self.get_prefix(message) invoked_prefix = prefix if isinstance(prefix, str): @@ -867,8 +858,7 @@ class BotBase(GroupMixin): ctx.command = self.all_commands.get(invoker) return ctx - @asyncio.coroutine - def invoke(self, ctx): + async def invoke(self, ctx): """|coro| Invokes the command given under the invocation context and @@ -882,18 +872,17 @@ class BotBase(GroupMixin): if ctx.command is not None: self.dispatch('command', ctx) try: - if (yield from self.can_run(ctx, call_once=True)): - yield from ctx.command.invoke(ctx) + if (await self.can_run(ctx, call_once=True)): + await ctx.command.invoke(ctx) except CommandError as e: - yield from ctx.command.dispatch_error(ctx, e) + await ctx.command.dispatch_error(ctx, e) else: self.dispatch('command_completion', ctx) elif ctx.invoked_with: exc = CommandNotFound('Command "{}" is not found'.format(ctx.invoked_with)) self.dispatch('command_error', ctx, exc) - @asyncio.coroutine - def process_commands(self, message): + async def process_commands(self, message): """|coro| This function processes the commands that have been registered @@ -912,12 +901,11 @@ class BotBase(GroupMixin): message : discord.Message The message to process commands for. """ - ctx = yield from self.get_context(message) - yield from self.invoke(ctx) + ctx = await self.get_context(message) + await self.invoke(ctx) - @asyncio.coroutine - def on_message(self, message): - yield from self.process_commands(message) + async def on_message(self, message): + await self.process_commands(message) class Bot(BotBase, discord.Client): """Represents a discord bot. diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index 303c1051b..dfe1a9edb 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -86,8 +86,7 @@ class Context(discord.abc.Messageable): self.command_failed = attrs.pop('command_failed', False) self._state = self.message._state - @asyncio.coroutine - def invoke(self, *args, **kwargs): + async def invoke(self, *args, **kwargs): """|coro| Calls a command with the arguments given. @@ -125,11 +124,10 @@ class Context(discord.abc.Messageable): arguments.append(self) arguments.extend(args[1:]) - ret = yield from command.callback(*arguments, **kwargs) + ret = await command.callback(*arguments, **kwargs) return ret - @asyncio.coroutine - def reinvoke(self, *, call_hooks=False, restart=True): + async def reinvoke(self, *, call_hooks=False, restart=True): """|coro| Calls the command again. @@ -174,7 +172,7 @@ class Context(discord.abc.Messageable): to_call = cmd try: - yield from to_call.reinvoke(self, call_hooks=call_hooks) + await to_call.reinvoke(self, call_hooks=call_hooks) finally: self.command = cmd view.index = index @@ -188,8 +186,7 @@ class Context(discord.abc.Messageable): """Checks if the invocation context is valid to be invoked with.""" return self.prefix is not None and self.command is not None - @asyncio.coroutine - def _get_channel(self): + async def _get_channel(self): return self.channel @property diff --git a/discord/ext/commands/converter.py b/discord/ext/commands/converter.py index 9e3718b09..72b2cbf85 100644 --- a/discord/ext/commands/converter.py +++ b/discord/ext/commands/converter.py @@ -57,8 +57,7 @@ class Converter: method to do its conversion logic. This method must be a coroutine. """ - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): """|coro| The method to override to do conversion logic. @@ -99,8 +98,7 @@ class MemberConverter(IDConverter): 5. Lookup by nickname """ - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): message = ctx.message bot = ctx.bot match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument) @@ -136,8 +134,7 @@ class UserConverter(IDConverter): 3. Lookup by name#discrim 4. Lookup by name """ - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): match = self._get_id_match(argument) or re.match(r'<@!?([0-9]+)>$', argument) result = None state = ctx._state @@ -176,8 +173,7 @@ class TextChannelConverter(IDConverter): 2. Lookup by mention. 3. Lookup by name """ - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): bot = ctx.bot match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument) @@ -216,8 +212,7 @@ class VoiceChannelConverter(IDConverter): 2. Lookup by mention. 3. Lookup by name """ - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): bot = ctx.bot match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument) result = None @@ -255,8 +250,7 @@ class CategoryChannelConverter(IDConverter): 2. Lookup by mention. 3. Lookup by name """ - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): bot = ctx.bot match = self._get_id_match(argument) or re.match(r'<#([0-9]+)>$', argument) @@ -295,8 +289,7 @@ class ColourConverter(Converter): - The ``_`` in the name can be optionally replaced with spaces. """ - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): arg = argument.replace('0x', '').lower() if arg[0] == '#': @@ -323,8 +316,7 @@ class RoleConverter(IDConverter): 2. Lookup by mention. 3. Lookup by name """ - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): guild = ctx.message.guild if not guild: raise NoPrivateMessage() @@ -338,8 +330,7 @@ class RoleConverter(IDConverter): class GameConverter(Converter): """Converts to :class:`Game`.""" - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): return discord.Game(name=argument) class InviteConverter(Converter): @@ -347,10 +338,9 @@ class InviteConverter(Converter): This is done via an HTTP request using :meth:`.Bot.get_invite`. """ - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): try: - invite = yield from ctx.bot.get_invite(argument) + invite = await ctx.bot.get_invite(argument) return invite except Exception as e: raise BadArgument('Invite is invalid or expired') from e @@ -368,8 +358,7 @@ class EmojiConverter(IDConverter): 2. Lookup by extracting ID from the emoji. 3. Lookup by name """ - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): match = self._get_id_match(argument) or re.match(r'$', argument) result = None bot = ctx.bot @@ -403,8 +392,7 @@ class PartialEmojiConverter(Converter): This is done by extracting the animated flag, name and ID from the emoji. """ - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): match = re.match(r'<(a?):([a-zA-Z0-9\_]+):([0-9]+)>$', argument) if match: @@ -436,8 +424,7 @@ class clean_content(Converter): self.use_nicknames = use_nicknames self.escape_markdown = escape_markdown - @asyncio.coroutine - def convert(self, ctx, argument): + async def convert(self, ctx, argument): message = ctx.message transformations = {} diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 927fe6fa6..6c1d9a931 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -41,10 +41,9 @@ __all__ = [ 'Command', 'Group', 'GroupMixin', 'command', 'group', def wrap_callback(coro): @functools.wraps(coro) - @asyncio.coroutine - def wrapped(*args, **kwargs): + async def wrapped(*args, **kwargs): try: - ret = yield from coro(*args, **kwargs) + ret = await coro(*args, **kwargs) except CommandError: raise except asyncio.CancelledError: @@ -56,10 +55,9 @@ def wrap_callback(coro): def hooked_wrapped_callback(command, ctx, coro): @functools.wraps(coro) - @asyncio.coroutine - def wrapped(*args, **kwargs): + async def wrapped(*args, **kwargs): try: - ret = yield from coro(*args, **kwargs) + ret = await coro(*args, **kwargs) except CommandError: ctx.command_failed = True raise @@ -70,7 +68,7 @@ def hooked_wrapped_callback(command, ctx, coro): ctx.command_failed = True raise CommandInvokeError(e) from e finally: - yield from command.call_after_hooks(ctx) + await command.call_after_hooks(ctx) return ret return wrapped @@ -182,8 +180,7 @@ class Command: self._before_invoke = None self._after_invoke = None - @asyncio.coroutine - def dispatch_error(self, ctx, error): + async def dispatch_error(self, ctx, error): ctx.command_failed = True cog = self.instance try: @@ -193,9 +190,9 @@ class Command: else: injected = wrap_callback(coro) if cog is not None: - yield from injected(cog, ctx, error) + await injected(cog, ctx, error) else: - yield from injected(ctx, error) + await injected(ctx, error) try: local = getattr(cog, '_{0.__class__.__name__}__error'.format(cog)) @@ -203,7 +200,7 @@ class Command: pass else: wrapped = wrap_callback(local) - yield from wrapped(ctx, error) + await wrapped(ctx, error) finally: ctx.bot.dispatch('command_error', ctx, error) @@ -212,8 +209,7 @@ class Command: self.instance = instance return self - @asyncio.coroutine - def do_conversion(self, ctx, converter, argument): + async def do_conversion(self, ctx, converter, argument): if converter is bool: return _convert_to_bool(argument) @@ -228,15 +224,15 @@ class Command: if inspect.isclass(converter): if issubclass(converter, converters.Converter): instance = converter() - ret = yield from instance.convert(ctx, argument) + ret = await instance.convert(ctx, argument) return ret else: method = getattr(converter, 'convert', None) if method is not None and inspect.ismethod(method): - ret = yield from method(ctx, argument) + ret = await method(ctx, argument) return ret elif isinstance(converter, converters.Converter): - ret = yield from converter.convert(ctx, argument) + ret = await converter.convert(ctx, argument) return ret return converter(argument) @@ -250,8 +246,7 @@ class Command: converter = str return converter - @asyncio.coroutine - def transform(self, ctx, param): + async def transform(self, ctx, param): required = param.default is param.empty converter = self._get_converter(param) consume_rest_is_special = param.kind == param.KEYWORD_ONLY and not self.rest_is_raw @@ -271,7 +266,7 @@ class Command: argument = quoted_word(view) try: - return (yield from self.do_conversion(ctx, converter, argument)) + return (await self.do_conversion(ctx, converter, argument)) except CommandError as e: raise e except Exception as e: @@ -354,8 +349,7 @@ class Command: def __str__(self): return self.qualified_name - @asyncio.coroutine - def _parse_arguments(self, ctx): + async def _parse_arguments(self, ctx): ctx.args = [ctx] if self.instance is None else [self.instance, ctx] ctx.kwargs = {} args = ctx.args @@ -382,21 +376,21 @@ class Command: for name, param in iterator: if param.kind == param.POSITIONAL_OR_KEYWORD: - transformed = yield from self.transform(ctx, param) + transformed = await self.transform(ctx, param) args.append(transformed) elif param.kind == param.KEYWORD_ONLY: # kwarg only param denotes "consume rest" semantics if self.rest_is_raw: converter = self._get_converter(param) argument = view.read_rest() - kwargs[name] = yield from self.do_conversion(ctx, converter, argument) + kwargs[name] = await self.do_conversion(ctx, converter, argument) else: - kwargs[name] = yield from self.transform(ctx, param) + kwargs[name] = await self.transform(ctx, param) break elif param.kind == param.VAR_POSITIONAL: while not view.eof: try: - transformed = yield from self.transform(ctx, param) + transformed = await self.transform(ctx, param) args.append(transformed) except RuntimeError: break @@ -405,24 +399,22 @@ class Command: if not view.eof: raise TooManyArguments('Too many arguments passed to ' + self.qualified_name) - @asyncio.coroutine - def _verify_checks(self, ctx): + async def _verify_checks(self, ctx): if not self.enabled: raise DisabledCommand('{0.name} command is disabled'.format(self)) - if not (yield from self.can_run(ctx)): + if not (await self.can_run(ctx)): raise CheckFailure('The check functions for command {0.qualified_name} failed.'.format(self)) - @asyncio.coroutine - def call_before_hooks(self, ctx): + async def call_before_hooks(self, ctx): # now that we're done preparing we can call the pre-command hooks # first, call the command local hook: cog = self.instance if self._before_invoke is not None: if cog is None: - yield from self._before_invoke(ctx) + await self._before_invoke(ctx) else: - yield from self._before_invoke(cog, ctx) + await self._before_invoke(cog, ctx) # call the cog local hook if applicable: try: @@ -430,37 +422,35 @@ class Command: except AttributeError: pass else: - yield from hook(ctx) + await hook(ctx) # call the bot global hook if necessary hook = ctx.bot._before_invoke if hook is not None: - yield from hook(ctx) + await hook(ctx) - @asyncio.coroutine - def call_after_hooks(self, ctx): + async def call_after_hooks(self, ctx): cog = self.instance if self._after_invoke is not None: if cog is None: - yield from self._after_invoke(ctx) + await self._after_invoke(ctx) else: - yield from self._after_invoke(cog, ctx) + await self._after_invoke(cog, ctx) try: hook = getattr(cog, '_{0.__class__.__name__}__after_invoke'.format(cog)) except AttributeError: pass else: - yield from hook(ctx) + await hook(ctx) hook = ctx.bot._after_invoke if hook is not None: - yield from hook(ctx) + await hook(ctx) - @asyncio.coroutine - def prepare(self, ctx): + async def prepare(self, ctx): ctx.command = self - yield from self._verify_checks(ctx) + await self._verify_checks(ctx) if self._buckets.valid: bucket = self._buckets.get_bucket(ctx.message) @@ -468,8 +458,8 @@ class Command: if retry_after: raise CommandOnCooldown(bucket, retry_after) - yield from self._parse_arguments(ctx) - yield from self.call_before_hooks(ctx) + await self._parse_arguments(ctx) + await self.call_before_hooks(ctx) def is_on_cooldown(self, ctx): """Checks whether the command is currently on cooldown. @@ -502,34 +492,32 @@ class Command: bucket = self._buckets.get_bucket(ctx.message) bucket.reset() - @asyncio.coroutine - def invoke(self, ctx): - yield from self.prepare(ctx) + async def invoke(self, ctx): + await self.prepare(ctx) # terminate the invoked_subcommand chain. # since we're in a regular command (and not a group) then # the invoked subcommand is None. ctx.invoked_subcommand = None injected = hooked_wrapped_callback(self, ctx, self.callback) - yield from injected(*ctx.args, **ctx.kwargs) + await injected(*ctx.args, **ctx.kwargs) - @asyncio.coroutine - def reinvoke(self, ctx, *, call_hooks=False): + async def reinvoke(self, ctx, *, call_hooks=False): ctx.command = self - yield from self._parse_arguments(ctx) + await self._parse_arguments(ctx) if call_hooks: - yield from self.call_before_hooks(ctx) + await self.call_before_hooks(ctx) ctx.invoked_subcommand = None try: - yield from self.callback(*ctx.args, **ctx.kwargs) + await self.callback(*ctx.args, **ctx.kwargs) except: ctx.command_failed = True raise finally: if call_hooks: - yield from self.call_after_hooks(ctx) + await self.call_after_hooks(ctx) def error(self, coro): """A decorator that registers a coroutine as a local error handler. @@ -667,8 +655,7 @@ class Command: return ' '.join(result) - @asyncio.coroutine - def can_run(self, ctx): + async def can_run(self, ctx): """|coro| Checks if the command can be executed by checking all the predicates @@ -695,7 +682,7 @@ class Command: ctx.command = self try: - if not (yield from ctx.bot.can_run(ctx)): + if not (await ctx.bot.can_run(ctx)): raise CheckFailure('The global check functions for command {0.qualified_name} failed.'.format(self)) cog = self.instance @@ -705,7 +692,7 @@ class Command: except AttributeError: pass else: - ret = yield from discord.utils.maybe_coroutine(local_check, ctx) + ret = await discord.utils.maybe_coroutine(local_check, ctx) if not ret: return False @@ -714,7 +701,7 @@ class Command: # since we have no checks, then we just return True. return True - return (yield from discord.utils.async_all(predicate(ctx) for predicate in predicates)) + return (await discord.utils.async_all(predicate(ctx) for predicate in predicates)) finally: ctx.command = original @@ -903,11 +890,10 @@ class Group(GroupMixin, Command): self.invoke_without_command = attrs.pop('invoke_without_command', False) super().__init__(**attrs) - @asyncio.coroutine - def invoke(self, ctx): + async def invoke(self, ctx): early_invoke = not self.invoke_without_command if early_invoke: - yield from self.prepare(ctx) + await self.prepare(ctx) view = ctx.view previous = view.index @@ -920,26 +906,25 @@ class Group(GroupMixin, Command): if early_invoke: injected = hooked_wrapped_callback(self, ctx, self.callback) - yield from injected(*ctx.args, **ctx.kwargs) + await injected(*ctx.args, **ctx.kwargs) if trigger and ctx.invoked_subcommand: ctx.invoked_with = trigger - yield from ctx.invoked_subcommand.invoke(ctx) + await ctx.invoked_subcommand.invoke(ctx) elif not early_invoke: # undo the trigger parsing view.index = previous view.previous = previous - yield from super().invoke(ctx) + await super().invoke(ctx) - @asyncio.coroutine - def reinvoke(self, ctx, *, call_hooks=False): + async def reinvoke(self, ctx, *, call_hooks=False): early_invoke = not self.invoke_without_command if early_invoke: ctx.command = self - yield from self._parse_arguments(ctx) + await self._parse_arguments(ctx) if call_hooks: - yield from self.call_before_hooks(ctx) + await self.call_before_hooks(ctx) view = ctx.view previous = view.index @@ -952,22 +937,22 @@ class Group(GroupMixin, Command): if early_invoke: try: - yield from self.callback(*ctx.args, **ctx.kwargs) + await self.callback(*ctx.args, **ctx.kwargs) except: ctx.command_failed = True raise finally: if call_hooks: - yield from self.call_after_hooks(ctx) + await self.call_after_hooks(ctx) if trigger and ctx.invoked_subcommand: ctx.invoked_with = trigger - yield from ctx.invoked_subcommand.reinvoke(ctx, call_hooks=call_hooks) + await ctx.invoked_subcommand.reinvoke(ctx, call_hooks=call_hooks) elif not early_invoke: # undo the trigger parsing view.index = previous view.previous = previous - yield from super().reinvoke(ctx, call_hooks=call_hooks) + await super().reinvoke(ctx, call_hooks=call_hooks) # Decorators @@ -1279,9 +1264,8 @@ def is_owner(): from :exc:`.CheckFailure`. """ - @asyncio.coroutine - def predicate(ctx): - if not (yield from ctx.bot.is_owner(ctx.author)): + async def predicate(ctx): + if not (await ctx.bot.is_owner(ctx.author)): raise NotOwner('You do not own this bot.') return True diff --git a/discord/ext/commands/formatter.py b/discord/ext/commands/formatter.py index 7b9528c46..0dead85f7 100644 --- a/discord/ext/commands/formatter.py +++ b/discord/ext/commands/formatter.py @@ -199,8 +199,7 @@ class HelpFormatter: return "Type {0}{1} command for more info on a command.\n" \ "You can also type {0}{1} category for more info on a category.".format(self.clean_prefix, command_name) - @asyncio.coroutine - def filter_command_list(self): + async def filter_command_list(self): """Returns a filtered list of commands based on the two attributes provided, :attr:`show_check_failure` and :attr:`show_hidden`. Also filters based on if :meth:`~.HelpFormatter.is_cog` is valid. @@ -224,14 +223,13 @@ class HelpFormatter: return True - @asyncio.coroutine - def predicate(tup): + async def predicate(tup): if sane_no_suspension_point_predicate(tup) is False: return False cmd = tup[1] try: - return (yield from cmd.can_run(self.context)) + return (await cmd.can_run(self.context)) except CommandError: return False @@ -242,7 +240,7 @@ class HelpFormatter: # Gotta run every check and verify it ret = [] for elem in iterator: - valid = yield from predicate(elem) + valid = await predicate(elem) if valid: ret.append(elem) @@ -258,8 +256,7 @@ class HelpFormatter: shortened = self.shorten(entry) self._paginator.add_line(shortened) - @asyncio.coroutine - def format_help_for(self, context, command_or_bot): + async def format_help_for(self, context, command_or_bot): """Formats the help page and handles the actual heavy lifting of how the help command looks like. To change the behaviour, override the :meth:`~.HelpFormatter.format` method. @@ -278,10 +275,9 @@ class HelpFormatter: """ self.context = context self.command = command_or_bot - return (yield from self.format()) + return (await self.format()) - @asyncio.coroutine - def format(self): + async def format(self): """Handles the actual behaviour involved with formatting. To change the behaviour, this method should be overridden. @@ -323,7 +319,7 @@ class HelpFormatter: # last place sorting position. return cog + ':' if cog is not None else '\u200bNo Category:' - filtered = yield from self.filter_command_list() + filtered = await self.filter_command_list() if self.is_bot(): data = sorted(filtered, key=category) for category, commands in itertools.groupby(data, key=category): diff --git a/discord/gateway.py b/discord/gateway.py index 4e100753f..d463ff737 100644 --- a/discord/gateway.py +++ b/discord/gateway.py @@ -71,7 +71,7 @@ class KeepAliveHandler(threading.Thread): if self._last_ack + self.heartbeat_timeout < time.monotonic(): log.warn("Shard ID %s has stopped responding to the gateway. Closing and restarting." % self.shard_id) coro = self.ws.close(4000) - f = compat.run_coroutine_threadsafe(coro, loop=self.ws.loop) + f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop) try: f.result() @@ -84,7 +84,7 @@ class KeepAliveHandler(threading.Thread): data = self.get_payload() log.debug(self.msg, data['d']) coro = self.ws.send_as_json(data) - f = compat.run_coroutine_threadsafe(coro, loop=self.ws.loop) + f = asyncio.run_coroutine_threadsafe(coro, loop=self.ws.loop) try: # block until sending is complete f.result() @@ -190,14 +190,13 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol): self._buffer = bytearray() @classmethod - @asyncio.coroutine - def from_client(cls, client, *, shard_id=None, session=None, sequence=None, resume=False): + async def from_client(cls, client, *, shard_id=None, session=None, sequence=None, resume=False): """Creates a main websocket for Discord from a :class:`Client`. This is for internal use only. """ - gateway = yield from client.http.get_gateway() - ws = yield from websockets.connect(gateway, loop=client.loop, klass=cls) + gateway = await client.http.get_gateway() + ws = await websockets.connect(gateway, loop=client.loop, klass=cls) # dynamically add attributes needed ws.token = client.http.token @@ -215,19 +214,19 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol): log.info('Created websocket connected to %s', gateway) # poll event for OP Hello - yield from ws.poll_event() + await ws.poll_event() if not resume: - yield from ws.identify() + await ws.identify() return ws - yield from ws.resume() + await ws.resume() try: - yield from ws.ensure_open() + await ws.ensure_open() except websockets.exceptions.ConnectionClosed: # ws got closed so let's just do a regular IDENTIFY connect. log.info('RESUME failed (the websocket decided to close) for Shard ID %s. Retrying.', shard_id) - return (yield from cls.from_client(client, shard_id=shard_id)) + return (await cls.from_client(client, shard_id=shard_id)) else: return ws @@ -251,13 +250,12 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol): A future to wait for. """ - future = compat.create_future(self.loop) + future = self.loop.create_future() entry = EventListener(event=event, predicate=predicate, result=result, future=future) self._dispatch_listeners.append(entry) return future - @asyncio.coroutine - def identify(self): + async def identify(self): """Sends the IDENTIFY packet.""" payload = { 'op': self.IDENTIFY, @@ -291,11 +289,10 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol): 'afk': False } - yield from self.send_as_json(payload) + await self.send_as_json(payload) log.info('Shard ID %s has sent the IDENTIFY payload.', self.shard_id) - @asyncio.coroutine - def resume(self): + async def resume(self): """Sends the RESUME packet.""" payload = { 'op': self.RESUME, @@ -306,11 +303,10 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol): } } - yield from self.send_as_json(payload) + await self.send_as_json(payload) log.info('Shard ID %s has sent the RESUME payload.', self.shard_id) - @asyncio.coroutine - def received_message(self, msg): + async def received_message(self, msg): self._dispatch('socket_raw_receive', msg) if isinstance(msg, bytes): @@ -342,7 +338,7 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol): # so we terminate our connection and raise an # internal exception signalling to reconnect. log.info('Received RECONNECT opcode.') - yield from self.close() + await self.close() raise ResumeWebSocket(self.shard_id) if op == self.HEARTBEAT_ACK: @@ -351,27 +347,27 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol): if op == self.HEARTBEAT: beat = self._keep_alive.get_payload() - yield from self.send_as_json(beat) + await self.send_as_json(beat) return if op == self.HELLO: interval = data['heartbeat_interval'] / 1000.0 self._keep_alive = KeepAliveHandler(ws=self, interval=interval, shard_id=self.shard_id) # send a heartbeat immediately - yield from self.send_as_json(self._keep_alive.get_payload()) + await self.send_as_json(self._keep_alive.get_payload()) self._keep_alive.start() return if op == self.INVALIDATE_SESSION: if data == True: - yield from asyncio.sleep(5.0, loop=self.loop) - yield from self.close() + await asyncio.sleep(5.0, loop=self.loop) + await self.close() raise ResumeWebSocket(self.shard_id) self.sequence = None self.session_id = None log.info('Shard ID %s session has been invalidated.' % self.shard_id) - yield from self.identify() + await self.identify() return if op != self.DISPATCH: @@ -435,8 +431,7 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol): def _can_handle_close(self, code): return code not in (1000, 4004, 4010, 4011) - @asyncio.coroutine - def poll_event(self): + async def poll_event(self): """Polls for a DISPATCH event and handles the general gateway loop. Raises @@ -445,8 +440,8 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol): The websocket connection was terminated for unhandled reasons. """ try: - msg = yield from self.recv() - yield from self.received_message(msg) + msg = await self.recv() + await self.received_message(msg) except websockets.exceptions.ConnectionClosed as e: if self._can_handle_close(e.code): log.info('Websocket closed with %s (%s), attempting a reconnect.', e.code, e.reason) @@ -455,21 +450,18 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol): log.info('Websocket closed with %s (%s), cannot reconnect.', e.code, e.reason) raise ConnectionClosed(e, shard_id=self.shard_id) from e - @asyncio.coroutine - def send(self, data): + async def send(self, data): self._dispatch('socket_raw_send', data) - yield from super().send(data) + await super().send(data) - @asyncio.coroutine - def send_as_json(self, data): + async def send_as_json(self, data): try: - yield from super().send(utils.to_json(data)) + await super().send(utils.to_json(data)) except websockets.exceptions.ConnectionClosed as e: if not self._can_handle_close(e.code): raise ConnectionClosed(e, shard_id=self.shard_id) from e - @asyncio.coroutine - def change_presence(self, *, activity=None, status=None, afk=False, since=0.0): + async def change_presence(self, *, activity=None, status=None, afk=False, since=0.0): if activity is not None: if not isinstance(activity, _ActivityTag): raise InvalidArgument('activity must be one of Game, Streaming, or Activity.') @@ -490,18 +482,16 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol): sent = utils.to_json(payload) log.debug('Sending "%s" to change status', sent) - yield from self.send(sent) + await self.send(sent) - @asyncio.coroutine - def request_sync(self, guild_ids): + async def request_sync(self, guild_ids): payload = { 'op': self.GUILD_SYNC, 'd': list(guild_ids) } - yield from self.send_as_json(payload) + await self.send_as_json(payload) - @asyncio.coroutine - def voice_state(self, guild_id, channel_id, self_mute=False, self_deaf=False): + async def voice_state(self, guild_id, channel_id, self_mute=False, self_deaf=False): payload = { 'op': self.VOICE_STATE, 'd': { @@ -513,14 +503,13 @@ class DiscordWebSocket(websockets.client.WebSocketClientProtocol): } log.debug('Updating our voice state to %s.', payload) - yield from self.send_as_json(payload) + await self.send_as_json(payload) - @asyncio.coroutine - def close_connection(self, *args, **kwargs): + async def close_connection(self, *args, **kwargs): if self._keep_alive: self._keep_alive.stop() - yield from super().close_connection(*args, **kwargs) + await super().close_connection(*args, **kwargs) class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): """Implements the websocket protocol for handling voice connections. @@ -565,13 +554,11 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): self.max_size = None self._keep_alive = None - @asyncio.coroutine - def send_as_json(self, data): + async def send_as_json(self, data): log.debug('Sending voice websocket frame: %s.', data) - yield from self.send(utils.to_json(data)) + await self.send(utils.to_json(data)) - @asyncio.coroutine - def resume(self): + async def resume(self): state = self._connection payload = { 'op': self.RESUME, @@ -581,10 +568,9 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): 'session_id': state.session_id } } - yield from self.send_as_json(payload) + await self.send_as_json(payload) - @asyncio.coroutine - def identify(self): + async def identify(self): state = self._connection payload = { 'op': self.IDENTIFY, @@ -595,27 +581,25 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): 'token': state.token } } - yield from self.send_as_json(payload) + await self.send_as_json(payload) @classmethod - @asyncio.coroutine - def from_client(cls, client, *, resume=False): + async def from_client(cls, client, *, resume=False): """Creates a voice websocket for the :class:`VoiceClient`.""" gateway = 'wss://' + client.endpoint + '/?v=3' - ws = yield from websockets.connect(gateway, loop=client.loop, klass=cls) + ws = await websockets.connect(gateway, loop=client.loop, klass=cls) ws.gateway = gateway ws._connection = client ws._max_heartbeat_timeout = 60.0 if resume: - yield from ws.resume() + await ws.resume() else: - yield from ws.identify() + await ws.identify() return ws - @asyncio.coroutine - def select_protocol(self, ip, port): + async def select_protocol(self, ip, port): payload = { 'op': self.SELECT_PROTOCOL, 'd': { @@ -628,10 +612,9 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): } } - yield from self.send_as_json(payload) + await self.send_as_json(payload) - @asyncio.coroutine - def speak(self, is_speaking=True): + async def speak(self, is_speaking=True): payload = { 'op': self.SPEAKING, 'd': { @@ -640,10 +623,9 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): } } - yield from self.send_as_json(payload) + await self.send_as_json(payload) - @asyncio.coroutine - def received_message(self, msg): + async def received_message(self, msg): log.debug('Voice websocket frame received: %s', msg) op = msg['op'] data = msg.get('d') @@ -652,17 +634,16 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): interval = data['heartbeat_interval'] / 1000.0 self._keep_alive = VoiceKeepAliveHandler(ws=self, interval=interval) self._keep_alive.start() - yield from self.initial_connection(data) + await self.initial_connection(data) elif op == self.HEARTBEAT_ACK: self._keep_alive.ack() elif op == self.INVALIDATE_SESSION: log.info('Voice RESUME failed.') - yield from self.identify() + await self.identify() elif op == self.SESSION_DESCRIPTION: - yield from self.load_secret_key(data) + await self.load_secret_key(data) - @asyncio.coroutine - def initial_connection(self, data): + async def initial_connection(self, data): state = self._connection state.ssrc = data['ssrc'] state.voice_port = data['port'] @@ -670,7 +651,7 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): packet = bytearray(70) struct.pack_into('>I', packet, 0, state.ssrc) state.socket.sendto(packet, (state.endpoint_ip, state.voice_port)) - recv = yield from self.loop.sock_recv(state.socket, 70) + recv = await self.loop.sock_recv(state.socket, 70) log.debug('received packet in initial_connection: %s', recv) # the ip is ascii starting at the 4th byte and ending at the first null @@ -683,28 +664,25 @@ class DiscordVoiceWebSocket(websockets.client.WebSocketClientProtocol): state.port = struct.unpack_from('= (3, 5) - class _AsyncIterator: __slots__ = () @@ -52,15 +50,14 @@ class _AsyncIterator: return self.find(predicate) - @asyncio.coroutine - def find(self, predicate): + async def find(self, predicate): while True: try: - elem = yield from self.next() + elem = await self.next() except NoMoreItems: return None - ret = yield from maybe_coroutine(predicate, elem) + ret = await maybe_coroutine(predicate, elem) if ret: return elem @@ -70,30 +67,26 @@ class _AsyncIterator: def filter(self, predicate): return _FilteredAsyncIterator(self, predicate) - @asyncio.coroutine - def flatten(self): + async def flatten(self): ret = [] while True: try: - item = yield from self.next() + item = await self.next() except NoMoreItems: return ret else: ret.append(item) - if PY35: - @asyncio.coroutine - def __aiter__(self): - return self + async def __aiter__(self): + return self - @asyncio.coroutine - def __anext__(self): - try: - msg = yield from self.next() - except NoMoreItems: - raise StopAsyncIteration() - else: - return msg + async def __anext__(self): + try: + msg = await self.next() + except NoMoreItems: + raise StopAsyncIteration() + else: + return msg def _identity(x): return x @@ -103,11 +96,10 @@ class _MappedAsyncIterator(_AsyncIterator): self.iterator = iterator self.func = func - @asyncio.coroutine - def next(self): + async def next(self): # this raises NoMoreItems and will propagate appropriately - item = yield from self.iterator.next() - return (yield from maybe_coroutine(self.func, item)) + item = await self.iterator.next() + return (await maybe_coroutine(self.func, item)) class _FilteredAsyncIterator(_AsyncIterator): def __init__(self, iterator, predicate): @@ -118,14 +110,13 @@ class _FilteredAsyncIterator(_AsyncIterator): self.predicate = predicate - @asyncio.coroutine - def next(self): + async def next(self): getter = self.iterator.next pred = self.predicate while True: # propagate NoMoreItems similar to _MappedAsyncIterator - item = yield from getter() - ret = yield from maybe_coroutine(pred, item) + item = await getter() + ret = await maybe_coroutine(pred, item) if ret: return item @@ -142,18 +133,16 @@ class ReactionIterator(_AsyncIterator): self.channel_id = message.channel.id self.users = asyncio.Queue(loop=state.loop) - @asyncio.coroutine - def next(self): + async def next(self): if self.users.empty(): - yield from self.fill_users() + await self.fill_users() try: return self.users.get_nowait() except asyncio.QueueEmpty: raise NoMoreItems() - @asyncio.coroutine - def fill_users(self): + async def fill_users(self): # this is a hack because >circular imports< from .user import User @@ -161,7 +150,7 @@ class ReactionIterator(_AsyncIterator): retrieve = self.limit if self.limit <= 100 else 100 after = self.after.id if self.after else None - data = yield from self.getter(self.message.id, self.channel_id, self.emoji, retrieve, after=after) + data = await self.getter(self.message.id, self.channel_id, self.emoji, retrieve, after=after) if data: self.limit -= retrieve @@ -169,15 +158,15 @@ class ReactionIterator(_AsyncIterator): if self.guild is None: for element in reversed(data): - yield from self.users.put(User(state=self.state, data=element)) + await self.users.put(User(state=self.state, data=element)) else: for element in reversed(data): member_id = int(element['id']) member = self.guild.get_member(member_id) if member is not None: - yield from self.users.put(member) + await self.users.put(member) else: - yield from self.users.put(User(state=self.state, data=element)) + await self.users.put(User(state=self.state, data=element)) class HistoryIterator(_AsyncIterator): """Iterator for receiving a channel's message history. @@ -270,10 +259,9 @@ class HistoryIterator(_AsyncIterator): else: self._retrieve_messages = self._retrieve_messages_before_strategy - @asyncio.coroutine - def next(self): + async def next(self): if self.messages.empty(): - yield from self.fill_messages() + await self.fill_messages() try: return self.messages.get_nowait() @@ -292,15 +280,14 @@ class HistoryIterator(_AsyncIterator): self.retrieve = r return r > 0 - @asyncio.coroutine - def flatten(self): + async def flatten(self): # this is similar to fill_messages except it uses a list instead # of a queue to place the messages in. result = [] - channel = yield from self.messageable._get_channel() + channel = await self.messageable._get_channel() self.channel = channel while self._get_retrieve(): - data = yield from self._retrieve_messages(self.retrieve) + data = await self._retrieve_messages(self.retrieve) if len(data) < 100: self.limit = 0 # terminate the infinite loop @@ -313,15 +300,14 @@ class HistoryIterator(_AsyncIterator): result.append(self.state.create_message(channel=channel, data=element)) return result - @asyncio.coroutine - def fill_messages(self): + async def fill_messages(self): if not hasattr(self, 'channel'): # do the required set up - channel = yield from self.messageable._get_channel() + channel = await self.messageable._get_channel() self.channel = channel if self._get_retrieve(): - data = yield from self._retrieve_messages(self.retrieve) + data = await self._retrieve_messages(self.retrieve) if self.limit is None and len(data) < 100: self.limit = 0 # terminate the infinite loop @@ -332,41 +318,37 @@ class HistoryIterator(_AsyncIterator): channel = self.channel for element in data: - yield from self.messages.put(self.state.create_message(channel=channel, data=element)) + await self.messages.put(self.state.create_message(channel=channel, data=element)) - @asyncio.coroutine - def _retrieve_messages(self, retrieve): + async def _retrieve_messages(self, retrieve): """Retrieve messages and update next parameters.""" pass - @asyncio.coroutine - def _retrieve_messages_before_strategy(self, retrieve): + async def _retrieve_messages_before_strategy(self, retrieve): """Retrieve messages using before parameter.""" before = self.before.id if self.before else None - data = yield from self.logs_from(self.channel.id, retrieve, before=before) + data = await self.logs_from(self.channel.id, retrieve, before=before) if len(data): if self.limit is not None: self.limit -= retrieve self.before = Object(id=int(data[-1]['id'])) return data - @asyncio.coroutine - def _retrieve_messages_after_strategy(self, retrieve): + async def _retrieve_messages_after_strategy(self, retrieve): """Retrieve messages using after parameter.""" after = self.after.id if self.after else None - data = yield from self.logs_from(self.channel.id, retrieve, after=after) + data = await self.logs_from(self.channel.id, retrieve, after=after) if len(data): if self.limit is not None: self.limit -= retrieve self.after = Object(id=int(data[0]['id'])) return data - @asyncio.coroutine - def _retrieve_messages_around_strategy(self, retrieve): + async def _retrieve_messages_around_strategy(self, retrieve): """Retrieve messages using around parameter.""" if self.around: around = self.around.id if self.around else None - data = yield from self.logs_from(self.channel.id, retrieve, around=around) + data = await self.logs_from(self.channel.id, retrieve, around=around) self.around = None return data return [] @@ -411,10 +393,9 @@ class AuditLogIterator(_AsyncIterator): else: self._strategy = self._before_strategy - @asyncio.coroutine - def _before_strategy(self, retrieve): + async def _before_strategy(self, retrieve): before = self.before.id if self.before else None - data = yield from self.request(self.guild.id, limit=retrieve, user_id=self.user_id, + data = await self.request(self.guild.id, limit=retrieve, user_id=self.user_id, action_type=self.action_type, before=before) entries = data.get('audit_log_entries', []) @@ -424,10 +405,9 @@ class AuditLogIterator(_AsyncIterator): self.before = Object(id=int(entries[-1]['id'])) return data.get('users', []), entries - @asyncio.coroutine - def _after_strategy(self, retrieve): + async def _after_strategy(self, retrieve): after = self.after.id if self.after else None - data = yield from self.request(self.guild.id, limit=retrieve, user_id=self.user_id, + data = await self.request(self.guild.id, limit=retrieve, user_id=self.user_id, action_type=self.action_type, after=after) entries = data.get('audit_log_entries', []) if len(data) and entries: @@ -436,10 +416,9 @@ class AuditLogIterator(_AsyncIterator): self.after = Object(id=int(entries[0]['id'])) return data.get('users', []), entries - @asyncio.coroutine - def next(self): + async def next(self): if self.entries.empty(): - yield from self._fill() + await self._fill() try: return self.entries.get_nowait() @@ -458,12 +437,11 @@ class AuditLogIterator(_AsyncIterator): self.retrieve = r return r > 0 - @asyncio.coroutine - def _fill(self): + async def _fill(self): from .user import User if self._get_retrieve(): - users, data = yield from self._strategy(self.retrieve) + users, data = await self._strategy(self.retrieve) if self.limit is None and len(data) < 100: self.limit = 0 # terminate the infinite loop @@ -481,4 +459,4 @@ class AuditLogIterator(_AsyncIterator): if element['action_type'] is None: continue - yield from self.entries.put(AuditLogEntry(data=element, users=self._users, guild=self.guild)) + await self.entries.put(AuditLogEntry(data=element, users=self._users, guild=self.guild)) diff --git a/discord/member.py b/discord/member.py index 11bbfcf4f..792313d31 100644 --- a/discord/member.py +++ b/discord/member.py @@ -183,9 +183,8 @@ class Member(discord.abc.Messageable, _BaseUser): def __hash__(self): return hash(self._user.id) - @asyncio.coroutine - def _get_channel(self): - ch = yield from self.create_dm() + async def _get_channel(self): + ch = await self.create_dm() return ch def _update_roles(self, data): @@ -341,32 +340,28 @@ class Member(discord.abc.Messageable, _BaseUser): """Optional[:class:`VoiceState`]: Returns the member's current voice state.""" return self.guild._voice_state_for(self._user.id) - @asyncio.coroutine - def ban(self, **kwargs): + async def ban(self, **kwargs): """|coro| Bans this member. Equivalent to :meth:`Guild.ban` """ - yield from self.guild.ban(self, **kwargs) + await self.guild.ban(self, **kwargs) - @asyncio.coroutine - def unban(self, *, reason=None): + async def unban(self, *, reason=None): """|coro| Unbans this member. Equivalent to :meth:`Guild.unban` """ - yield from self.guild.unban(self, reason=reason) + await self.guild.unban(self, reason=reason) - @asyncio.coroutine - def kick(self, *, reason=None): + async def kick(self, *, reason=None): """|coro| Kicks this member. Equivalent to :meth:`Guild.kick` """ - yield from self.guild.kick(self, reason=reason) + await self.guild.kick(self, reason=reason) - @asyncio.coroutine - def edit(self, *, reason=None, **fields): + async def edit(self, *, reason=None, **fields): """|coro| Edits the member's data. @@ -423,7 +418,7 @@ class Member(discord.abc.Messageable, _BaseUser): else: nick = nick if nick else '' if self._state.self_id == self.id: - yield from http.change_my_nickname(guild_id, nick, reason=reason) + await http.change_my_nickname(guild_id, nick, reason=reason) else: payload['nick'] = nick @@ -449,12 +444,11 @@ class Member(discord.abc.Messageable, _BaseUser): else: payload['roles'] = tuple(r.id for r in roles) - yield from http.edit_member(guild_id, self.id, reason=reason, **payload) + await http.edit_member(guild_id, self.id, reason=reason, **payload) # TODO: wait for WS event for modify-in-place behaviour - @asyncio.coroutine - def move_to(self, channel, *, reason=None): + async def move_to(self, channel, *, reason=None): """|coro| Moves a member to a new voice channel (they must be connected first). @@ -471,10 +465,9 @@ class Member(discord.abc.Messageable, _BaseUser): reason: Optional[str] The reason for doing this action. Shows up on the audit log. """ - yield from self.edit(voice_channel=channel, reason=reason) + await self.edit(voice_channel=channel, reason=reason) - @asyncio.coroutine - def add_roles(self, *roles, reason=None, atomic=True): + async def add_roles(self, *roles, reason=None, atomic=True): """|coro| Gives the member a number of :class:`Role`\s. @@ -504,16 +497,15 @@ class Member(discord.abc.Messageable, _BaseUser): if not atomic: new_roles = utils._unique(Object(id=r.id) for s in (self.roles[1:], roles) for r in s) - yield from self.edit(roles=new_roles, reason=reason) + await self.edit(roles=new_roles, reason=reason) else: req = self._state.http.add_role guild_id = self.guild.id user_id = self.id for role in roles: - yield from req(guild_id, user_id, role.id, reason=reason) + await req(guild_id, user_id, role.id, reason=reason) - @asyncio.coroutine - def remove_roles(self, *roles, reason=None, atomic=True): + async def remove_roles(self, *roles, reason=None, atomic=True): """|coro| Removes :class:`Role`\s from this member. @@ -549,10 +541,10 @@ class Member(discord.abc.Messageable, _BaseUser): except ValueError: pass - yield from self.edit(roles=new_roles, reason=reason) + await self.edit(roles=new_roles, reason=reason) else: req = self._state.http.remove_role guild_id = self.guild.id user_id = self.id for role in roles: - yield from req(guild_id, user_id, role.id, reason=reason) + await req(guild_id, user_id, role.id, reason=reason) diff --git a/discord/message.py b/discord/message.py index e9ebf5f22..5e78be484 100644 --- a/discord/message.py +++ b/discord/message.py @@ -71,8 +71,7 @@ class Attachment: self.proxy_url = data.get('proxy_url') self._http = state.http - @asyncio.coroutine - def save(self, fp, *, seek_begin=True): + async def save(self, fp, *, seek_begin=True): """|coro| Saves this attachment into a file-like object. @@ -100,7 +99,7 @@ class Attachment: The number of bytes written. """ - data = yield from self._http.get_attachment(self.url) + data = await self._http.get_attachment(self.url) if isinstance(fp, str): with open(fp, 'wb') as f: return f.write(data) @@ -527,8 +526,7 @@ class Message: else: return '{0.author.name} started a call \N{EM DASH} Join the call.'.format(self) - @asyncio.coroutine - def delete(self): + async def delete(self): """|coro| Deletes the message. @@ -544,10 +542,9 @@ class Message: HTTPException Deleting the message failed. """ - yield from self._state.http.delete_message(self.channel.id, self.id) + await self._state.http.delete_message(self.channel.id, self.id) - @asyncio.coroutine - def edit(self, **fields): + async def edit(self, **fields): """|coro| Edits the message. @@ -589,7 +586,7 @@ class Message: if embed is not None: fields['embed'] = embed.to_dict() - data = yield from self._state.http.edit_message(self.id, self.channel.id, **fields) + data = await self._state.http.edit_message(self.id, self.channel.id, **fields) self._update(channel=self.channel, data=data) try: @@ -598,18 +595,16 @@ class Message: pass else: if delete_after is not None: - @asyncio.coroutine - def delete(): - yield from asyncio.sleep(delete_after, loop=self._state.loop) + async def delete(): + await asyncio.sleep(delete_after, loop=self._state.loop) try: - yield from self._state.http.delete_message(self.channel.id, self.id) + await self._state.http.delete_message(self.channel.id, self.id) except: pass - compat.create_task(delete(), loop=self._state.loop) + asyncio.ensure_future(delete(), loop=self._state.loop) - @asyncio.coroutine - def pin(self): + async def pin(self): """|coro| Pins the message. @@ -628,11 +623,10 @@ class Message: having more than 50 pinned messages. """ - yield from self._state.http.pin_message(self.channel.id, self.id) + await self._state.http.pin_message(self.channel.id, self.id) self.pinned = True - @asyncio.coroutine - def unpin(self): + async def unpin(self): """|coro| Unpins the message. @@ -650,11 +644,10 @@ class Message: Unpinning the message failed. """ - yield from self._state.http.unpin_message(self.channel.id, self.id) + await self._state.http.unpin_message(self.channel.id, self.id) self.pinned = False - @asyncio.coroutine - def add_reaction(self, emoji): + async def add_reaction(self, emoji): """|coro| Add a reaction to the message. @@ -694,10 +687,9 @@ class Message: else: raise InvalidArgument('emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.'.format(emoji)) - yield from self._state.http.add_reaction(self.id, self.channel.id, emoji) + await self._state.http.add_reaction(self.id, self.channel.id, emoji) - @asyncio.coroutine - def remove_reaction(self, emoji, member): + async def remove_reaction(self, emoji, member): """|coro| Remove a reaction by the member from the message. @@ -742,12 +734,11 @@ class Message: raise InvalidArgument('emoji argument must be str, Emoji, or Reaction not {.__class__.__name__}.'.format(emoji)) if member.id == self._state.self_id: - yield from self._state.http.remove_own_reaction(self.id, self.channel.id, emoji) + await self._state.http.remove_own_reaction(self.id, self.channel.id, emoji) else: - yield from self._state.http.remove_reaction(self.id, self.channel.id, emoji, member.id) + await self._state.http.remove_reaction(self.id, self.channel.id, emoji, member.id) - @asyncio.coroutine - def clear_reactions(self): + async def clear_reactions(self): """|coro| Removes all the reactions from the message. @@ -761,7 +752,7 @@ class Message: Forbidden You do not have the proper permissions to remove all the reactions. """ - yield from self._state.http.clear_reactions(self.id, self.channel.id) + await self._state.http.clear_reactions(self.id, self.channel.id) def ack(self): """|coro| diff --git a/discord/reaction.py b/discord/reaction.py index 2af747895..9fdc4b74d 100644 --- a/discord/reaction.py +++ b/discord/reaction.py @@ -137,7 +137,7 @@ class Reaction: iterator = reaction.users() while True: try: - user = yield from iterator.next() + user = await iterator.next() except discord.NoMoreItems: break else: diff --git a/discord/relationship.py b/discord/relationship.py index 575627e97..b301d420c 100644 --- a/discord/relationship.py +++ b/discord/relationship.py @@ -52,8 +52,7 @@ class Relationship: def __repr__(self): return ''.format(self) - @asyncio.coroutine - def delete(self): + async def delete(self): """|coro| Deletes the relationship. @@ -64,10 +63,9 @@ class Relationship: Deleting the relationship failed. """ - yield from self._state.http.remove_relationship(self.user.id) + await self._state.http.remove_relationship(self.user.id) - @asyncio.coroutine - def accept(self): + async def accept(self): """|coro| Accepts the relationship request. e.g. accepting a @@ -79,4 +77,4 @@ class Relationship: Accepting the relationship failed. """ - yield from self._state.http.add_relationship(self.user.id) + await self._state.http.add_relationship(self.user.id) diff --git a/discord/role.py b/discord/role.py index 3e35d8af9..5cee019be 100644 --- a/discord/role.py +++ b/discord/role.py @@ -171,8 +171,7 @@ class Role(Hashable): return [member for member in all_members if self in member.roles] - @asyncio.coroutine - def _move(self, position, reason): + async def _move(self, position, reason): if position <= 0: raise InvalidArgument("Cannot move role to position 0 or below") @@ -196,10 +195,9 @@ class Role(Hashable): roles.append(self.id) payload = [{"id": z[0], "position": z[1]} for z in zip(roles, change_range)] - yield from http.move_role_position(self.guild.id, payload, reason=reason) + await http.move_role_position(self.guild.id, payload, reason=reason) - @asyncio.coroutine - def edit(self, *, reason=None, **fields): + async def edit(self, *, reason=None, **fields): """|coro| Edits the role. @@ -240,7 +238,7 @@ class Role(Hashable): position = fields.get('position') if position is not None: - yield from self._move(position, reason=reason) + await self._move(position, reason=reason) self.position = position try: @@ -256,11 +254,10 @@ class Role(Hashable): 'mentionable': fields.get('mentionable', self.mentionable) } - data = yield from self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) + data = await self._state.http.edit_role(self.guild.id, self.id, reason=reason, **payload) self._update(data) - @asyncio.coroutine - def delete(self, *, reason=None): + async def delete(self, *, reason=None): """|coro| Deletes the role. @@ -281,4 +278,4 @@ class Role(Hashable): Deleting the role failed. """ - yield from self._state.http.delete_role(self.guild.id, self.id, reason=reason) + await self._state.http.delete_role(self.guild.id, self.id, reason=reason) diff --git a/discord/shard.py b/discord/shard.py index 284f5cdca..a7fec0c47 100644 --- a/discord/shard.py +++ b/discord/shard.py @@ -43,7 +43,7 @@ class Shard: self.ws = ws self._client = client self.loop = self._client.loop - self._current = compat.create_future(self.loop) + self._current = self.loop.create_future() self._current.set_result(None) # we just need an already done future self._pending = asyncio.Event(loop=self.loop) self._pending_task = None @@ -58,47 +58,36 @@ class Shard: def complete_pending_reads(self): self._pending.set() - def _pending_reads(self): + async def _pending_reads(self): try: while self.is_pending(): - yield from self.poll() + await self.poll() except asyncio.CancelledError: pass def launch_pending_reads(self): - self._pending_task = compat.create_task(self._pending_reads(), loop=self.loop) + self._pending_task = asyncio.ensure_future(self._pending_reads(), loop=self.loop) def wait(self): return self._pending_task - @asyncio.coroutine - def poll(self): + async def poll(self): try: - yield from self.ws.poll_event() + await self.ws.poll_event() except ResumeWebSocket as e: log.info('Got a request to RESUME the websocket at Shard ID %s.', self.id) coro = DiscordWebSocket.from_client(self._client, resume=True, shard_id=self.id, session=self.ws.session_id, sequence=self.ws.sequence) - self.ws = yield from asyncio.wait_for(coro, timeout=180.0, loop=self.loop) + self.ws = await asyncio.wait_for(coro, timeout=180.0, loop=self.loop) def get_future(self): if self._current.done(): - self._current = compat.create_task(self.poll(), loop=self.loop) + self._current = asyncio.ensure_future(self.poll(), loop=self.loop) return self._current -@asyncio.coroutine -def _ensure_coroutine_connect(gateway, loop): - # In 3.5+ websockets.connect does not return a coroutine, but an awaitable. - # The problem is that in 3.5.0 and in some cases 3.5.1, asyncio.ensure_future and - # by proxy, asyncio.wait_for, do not accept awaitables, but rather futures or coroutines. - # By wrapping it up into this function we ensure that it's in a coroutine and not an awaitable - # even for 3.5.0 users. - ws = yield from websockets.connect(gateway, loop=loop, klass=DiscordWebSocket) - return ws - class AutoShardedClient(Client): """A client similar to :class:`Client` except it handles the complications of sharding for the user into a more manageable and transparent single @@ -149,8 +138,7 @@ class AutoShardedClient(Client): self._connection._get_websocket = _get_websocket - @asyncio.coroutine - def _chunker(self, guild, *, shard_id=None): + async def _chunker(self, guild, *, shard_id=None): try: guild_id = guild.id shard_id = shard_id or guild.shard_id @@ -167,7 +155,7 @@ class AutoShardedClient(Client): } ws = self.shards[shard_id].ws - yield from ws.send_as_json(payload) + await ws.send_as_json(payload) @property def latency(self): @@ -189,8 +177,7 @@ class AutoShardedClient(Client): """ return [(shard_id, shard.ws.latency) for shard_id, shard in self.shards.items()] - @asyncio.coroutine - def request_offline_members(self, *guilds): + async def request_offline_members(self, *guilds): """|coro| Requests previously offline members from the guild to be filled up @@ -219,16 +206,16 @@ class AutoShardedClient(Client): _guilds = sorted(guilds, key=lambda g: g.shard_id) for shard_id, sub_guilds in itertools.groupby(_guilds, key=lambda g: g.shard_id): sub_guilds = list(sub_guilds) - yield from self._connection.request_offline_members(sub_guilds, shard_id=shard_id) + await self._connection.request_offline_members(sub_guilds, shard_id=shard_id) - @asyncio.coroutine - def launch_shard(self, gateway, shard_id): + async def launch_shard(self, gateway, shard_id): try: - ws = yield from asyncio.wait_for(_ensure_coroutine_connect(gateway, self.loop), loop=self.loop, timeout=180.0) + coro = websockets.connect(gateway, loop=self.loop, klass=DiscordWebSocket) + ws = await asyncio.wait_for(coro, loop=self.loop, timeout=180.0) except Exception as e: log.info('Failed to connect for shard_id: %s. Retrying...', shard_id) - yield from asyncio.sleep(5.0, loop=self.loop) - return (yield from self.launch_shard(gateway, shard_id)) + await asyncio.sleep(5.0, loop=self.loop) + return (await self.launch_shard(gateway, shard_id)) ws.token = self.http.token ws._connection = self._connection @@ -240,31 +227,30 @@ class AutoShardedClient(Client): try: # OP HELLO - yield from asyncio.wait_for(ws.poll_event(), loop=self.loop, timeout=180.0) - yield from asyncio.wait_for(ws.identify(), loop=self.loop, timeout=180.0) + await asyncio.wait_for(ws.poll_event(), loop=self.loop, timeout=180.0) + await asyncio.wait_for(ws.identify(), loop=self.loop, timeout=180.0) except asyncio.TimeoutError: log.info('Timed out when connecting for shard_id: %s. Retrying...', shard_id) - yield from asyncio.sleep(5.0, loop=self.loop) - return (yield from self.launch_shard(gateway, shard_id)) + await asyncio.sleep(5.0, loop=self.loop) + return (await self.launch_shard(gateway, shard_id)) # keep reading the shard while others connect self.shards[shard_id] = ret = Shard(ws, self) ret.launch_pending_reads() - yield from asyncio.sleep(5.0, loop=self.loop) + await asyncio.sleep(5.0, loop=self.loop) - @asyncio.coroutine - def launch_shards(self): + async def launch_shards(self): if self.shard_count is None: - self.shard_count, gateway = yield from self.http.get_bot_gateway() + self.shard_count, gateway = await self.http.get_bot_gateway() else: - gateway = yield from self.http.get_gateway() + gateway = await self.http.get_gateway() self._connection.shard_count = self.shard_count shard_ids = self.shard_ids if self.shard_ids else range(self.shard_count) for shard_id in shard_ids: - yield from self.launch_shard(gateway, shard_id) + await self.launch_shard(gateway, shard_id) shards_to_wait_for = [] for shard in self.shards.values(): @@ -272,21 +258,19 @@ class AutoShardedClient(Client): shards_to_wait_for.append(shard.wait()) # wait for all pending tasks to finish - yield from utils.sane_wait_for(shards_to_wait_for, timeout=300.0, loop=self.loop) + await utils.sane_wait_for(shards_to_wait_for, timeout=300.0, loop=self.loop) - @asyncio.coroutine - def _connect(self): - yield from self.launch_shards() + async def _connect(self): + await self.launch_shards() while True: pollers = [shard.get_future() for shard in self.shards.values()] - done, pending = yield from asyncio.wait(pollers, loop=self.loop, return_when=asyncio.FIRST_COMPLETED) + done, pending = await asyncio.wait(pollers, loop=self.loop, return_when=asyncio.FIRST_COMPLETED) for f in done: # we wanna re-raise to the main Client.connect handler if applicable f.result() - @asyncio.coroutine - def close(self): + async def close(self): """|coro| Closes the connection to discord. @@ -298,16 +282,15 @@ class AutoShardedClient(Client): for vc in self.voice_clients: try: - yield from vc.disconnect() + await vc.disconnect() except: pass to_close = [shard.ws.close() for shard in self.shards.values()] - yield from asyncio.wait(to_close, loop=self.loop) - yield from self.http.close() + await asyncio.wait(to_close, loop=self.loop) + await self.http.close() - @asyncio.coroutine - def change_presence(self, *, activity=None, status=None, afk=False, shard_id=None): + async def change_presence(self, *, activity=None, status=None, afk=False, shard_id=None): """|coro| Changes the client's presence. @@ -355,12 +338,12 @@ class AutoShardedClient(Client): if shard_id is None: for shard in self.shards.values(): - yield from shard.ws.change_presence(activity=activity, status=status, afk=afk) + await shard.ws.change_presence(activity=activity, status=status, afk=afk) guilds = self._connection.guilds else: shard = self.shards[shard_id] - yield from shard.ws.change_presence(activity=activity, status=status, afk=afk) + await shard.ws.change_presence(activity=activity, status=status, afk=afk) guilds = [g for g in self._connection.guilds if g.shard_id == shard_id] for guild in guilds: diff --git a/discord/state.py b/discord/state.py index 899ce029d..0e398583f 100644 --- a/discord/state.py +++ b/discord/state.py @@ -255,8 +255,7 @@ class ConnectionState: return channel, guild - @asyncio.coroutine - def request_offline_members(self, guilds): + async def request_offline_members(self, guilds): # get all the chunks chunks = [] for guild in guilds: @@ -265,17 +264,16 @@ class ConnectionState: # we only want to request ~75 guilds per chunk request. splits = [guilds[i:i + 75] for i in range(0, len(guilds), 75)] for split in splits: - yield from self.chunker(split) + await self.chunker(split) # wait for the chunks if chunks: try: - yield from utils.sane_wait_for(chunks, timeout=len(chunks) * 30.0, loop=self.loop) + await utils.sane_wait_for(chunks, timeout=len(chunks) * 30.0, loop=self.loop) except asyncio.TimeoutError: log.info('Somehow timed out waiting for chunks.') - @asyncio.coroutine - def _delay_ready(self): + async def _delay_ready(self): try: launch = self._ready_state.launch @@ -285,11 +283,11 @@ class ConnectionState: # this snippet of code is basically waiting 2 seconds # until the last GUILD_CREATE was sent launch.set() - yield from asyncio.sleep(2, loop=self.loop) + await asyncio.sleep(2, loop=self.loop) guilds = self._ready_state.guilds if self._fetch_offline: - yield from self.request_offline_members(guilds) + await self.request_offline_members(guilds) # remove the state try: @@ -300,7 +298,7 @@ class ConnectionState: # call GUILD_SYNC after we're done chunking if not self.is_bot: log.info('Requesting GUILD_SYNC for %s guilds', len(self.guilds)) - yield from self.syncer([s.id for s in self.guilds]) + await self.syncer([s.id for s in self.guilds]) except asyncio.CancelledError: pass else: @@ -336,7 +334,7 @@ class ConnectionState: self._add_private_channel(factory(me=self.user, data=pm, state=self)) self.dispatch('connect') - self._ready_task = compat.create_task(self._delay_ready(), loop=self.loop) + self._ready_task = asyncio.ensure_future(self._delay_ready(), loop=self.loop) def parse_resumed(self, data): self.dispatch('resumed') @@ -617,13 +615,12 @@ class ConnectionState: return self._add_guild_from_data(data) - @asyncio.coroutine - def _chunk_and_dispatch(self, guild, unavailable): + async def _chunk_and_dispatch(self, guild, unavailable): chunks = list(self.chunks_needed(guild)) - yield from self.chunker(guild) + await self.chunker(guild) if chunks: try: - yield from utils.sane_wait_for(chunks, timeout=len(chunks), loop=self.loop) + await utils.sane_wait_for(chunks, timeout=len(chunks), loop=self.loop) except asyncio.TimeoutError: log.info('Somehow timed out waiting for chunks.') @@ -662,7 +659,7 @@ class ConnectionState: # since we're not waiting for 'useful' READY we'll just # do the chunk request here if wanted if self._fetch_offline: - compat.create_task(self._chunk_and_dispatch(guild, unavailable), loop=self.loop) + asyncio.ensure_future(self._chunk_and_dispatch(guild, unavailable), loop=self.loop) return # Dispatch available if newly available @@ -807,7 +804,7 @@ class ConnectionState: vc = self._get_voice_client(key_id) if vc is not None: - compat.create_task(vc._create_socket(key_id, data)) + asyncio.ensure_future(vc._create_socket(key_id, data)) def parse_typing_start(self, data): channel, guild = self._get_guild_channel(data) @@ -886,7 +883,7 @@ class ConnectionState: return Message(state=self, channel=channel, data=data) def receive_chunk(self, guild_id): - future = compat.create_future(self.loop) + future = self.loop.create_future() listener = Listener(ListenerType.chunk, future, lambda s: s.id == guild_id) self._listeners.append(listener) return future @@ -896,8 +893,7 @@ class AutoShardedConnectionState(ConnectionState): super().__init__(*args, **kwargs) self._ready_task = None - @asyncio.coroutine - def request_offline_members(self, guilds, *, shard_id): + async def request_offline_members(self, guilds, *, shard_id): # get all the chunks chunks = [] for guild in guilds: @@ -906,30 +902,29 @@ class AutoShardedConnectionState(ConnectionState): # we only want to request ~75 guilds per chunk request. splits = [guilds[i:i + 75] for i in range(0, len(guilds), 75)] for split in splits: - yield from self.chunker(split, shard_id=shard_id) + await self.chunker(split, shard_id=shard_id) # wait for the chunks if chunks: try: - yield from utils.sane_wait_for(chunks, timeout=len(chunks) * 30.0, loop=self.loop) + await utils.sane_wait_for(chunks, timeout=len(chunks) * 30.0, loop=self.loop) except asyncio.TimeoutError: log.info('Somehow timed out waiting for chunks.') - @asyncio.coroutine - def _delay_ready(self): + async def _delay_ready(self): launch = self._ready_state.launch while not launch.is_set(): # this snippet of code is basically waiting 2 seconds # until the last GUILD_CREATE was sent launch.set() - yield from asyncio.sleep(2.0 * self.shard_count, loop=self.loop) + await asyncio.sleep(2.0 * self.shard_count, loop=self.loop) if self._fetch_offline: guilds = sorted(self._ready_state.guilds, key=lambda g: g.shard_id) for shard_id, sub_guilds in itertools.groupby(guilds, key=lambda g: g.shard_id): sub_guilds = list(sub_guilds) - yield from self.request_offline_members(sub_guilds, shard_id=shard_id) + await self.request_offline_members(sub_guilds, shard_id=shard_id) self.dispatch('shard_ready', shard_id) # remove the state @@ -964,4 +959,4 @@ class AutoShardedConnectionState(ConnectionState): self.dispatch('connect') if self._ready_task is None: - self._ready_task = compat.create_task(self._delay_ready(), loop=self.loop) + self._ready_task = asyncio.ensure_future(self._delay_ready(), loop=self.loop) diff --git a/discord/user.py b/discord/user.py index c43bae91d..a1f3704dc 100644 --- a/discord/user.py +++ b/discord/user.py @@ -313,8 +313,7 @@ class ClientUser(BaseUser): """Returns a :class:`list` of :class:`User`\s that the user has blocked.""" return [r.user for r in self._relationships.values() if r.type is RelationshipType.blocked] - @asyncio.coroutine - def edit(self, **fields): + async def edit(self, **fields): """|coro| Edits the current profile of the client. @@ -387,7 +386,7 @@ class ClientUser(BaseUser): http = self._state.http - data = yield from http.edit_profile(**args) + data = await http.edit_profile(**args) if not_bot_account: self.email = data['email'] try: @@ -398,8 +397,7 @@ class ClientUser(BaseUser): # manually update data by calling __init__ explicitly. self.__init__(state=self._state, data=data) - @asyncio.coroutine - def create_group(self, *recipients): + async def create_group(self, *recipients): """|coro| Creates a group direct message with the recipients @@ -434,7 +432,7 @@ class ClientUser(BaseUser): raise ClientException('You must have two or more recipients to create a group.') users = [str(u.id) for u in recipients] - data = yield from self._state.http.create_group(self.id, users) + data = await self._state.http.create_group(self.id, users) return GroupChannel(me=self, data=data, state=self._state) class User(BaseUser, discord.abc.Messageable): @@ -477,9 +475,8 @@ class User(BaseUser, discord.abc.Messageable): def __repr__(self): return ''.format(self) - @asyncio.coroutine - def _get_channel(self): - ch = yield from self.create_dm() + async def _get_channel(self): + ch = await self.create_dm() return ch @property @@ -491,8 +488,7 @@ class User(BaseUser, discord.abc.Messageable): """ return self._state._get_private_channel_by_user(self.id) - @asyncio.coroutine - def create_dm(self): + async def create_dm(self): """Creates a :class:`DMChannel` with this user. This should be rarely called, as this is done transparently for most @@ -503,7 +499,7 @@ class User(BaseUser, discord.abc.Messageable): return found state = self._state - data = yield from state.http.start_private_message(self.id) + data = await state.http.start_private_message(self.id) return state.add_dm_channel(data) @property @@ -525,8 +521,7 @@ class User(BaseUser, discord.abc.Messageable): return False return r.type is RelationshipType.blocked - @asyncio.coroutine - def block(self): + async def block(self): """|coro| Blocks the user. @@ -539,10 +534,9 @@ class User(BaseUser, discord.abc.Messageable): Blocking the user failed. """ - yield from self._state.http.add_relationship(self.id, type=RelationshipType.blocked.value) + await self._state.http.add_relationship(self.id, type=RelationshipType.blocked.value) - @asyncio.coroutine - def unblock(self): + async def unblock(self): """|coro| Unblocks the user. @@ -554,10 +548,9 @@ class User(BaseUser, discord.abc.Messageable): HTTPException Unblocking the user failed. """ - yield from self._state.http.remove_relationship(self.id) + await self._state.http.remove_relationship(self.id) - @asyncio.coroutine - def remove_friend(self): + async def remove_friend(self): """|coro| Removes the user as a friend. @@ -569,10 +562,9 @@ class User(BaseUser, discord.abc.Messageable): HTTPException Removing the user as a friend failed. """ - yield from self._state.http.remove_relationship(self.id) + await self._state.http.remove_relationship(self.id) - @asyncio.coroutine - def send_friend_request(self): + async def send_friend_request(self): """|coro| Sends the user a friend request. @@ -584,10 +576,9 @@ class User(BaseUser, discord.abc.Messageable): HTTPException Sending the friend request failed. """ - yield from self._state.http.send_friend_request(username=self.name, discriminator=self.discriminator) + await self._state.http.send_friend_request(username=self.name, discriminator=self.discriminator) - @asyncio.coroutine - def profile(self): + async def profile(self): """|coro| Gets the user's profile. This can only be used by non-bot accounts. @@ -606,7 +597,7 @@ class User(BaseUser, discord.abc.Messageable): """ state = self._state - data = yield from state.http.get_user_profile(self.id) + data = await state.http.get_user_profile(self.id) def transform(d): return state._get_guild(int(d['id'])) diff --git a/discord/utils.py b/discord/utils.py index ee612ecc9..5c939922e 100644 --- a/discord/utils.py +++ b/discord/utils.py @@ -29,6 +29,7 @@ from .errors import InvalidArgument import datetime from base64 import b64encode from email.utils import parsedate_to_datetime +from inspect import isawaitable as _isawaitable import asyncio import json import warnings, functools @@ -264,27 +265,23 @@ def _parse_ratelimit_header(request): reset = datetime.datetime.fromtimestamp(int(request.headers['X-Ratelimit-Reset']), datetime.timezone.utc) return (reset - now).total_seconds() -@asyncio.coroutine -def maybe_coroutine(f, *args, **kwargs): +async def maybe_coroutine(f, *args, **kwargs): value = f(*args, **kwargs) - if asyncio.iscoroutine(value): - return (yield from value) + if _isawaitable(value): + return (await value) else: return value -@asyncio.coroutine -def async_all(gen): - check = asyncio.iscoroutine +async def async_all(gen, *, check=_isawaitable): for elem in gen: if check(elem): - elem = yield from elem + elem = await elem if not elem: return False return True -@asyncio.coroutine -def sane_wait_for(futures, *, timeout, loop): - done, pending = yield from asyncio.wait(futures, timeout=timeout, loop=loop) +async def sane_wait_for(futures, *, timeout, loop): + done, pending = await asyncio.wait(futures, timeout=timeout, loop=loop) if len(pending) != 0: raise asyncio.TimeoutError() diff --git a/discord/voice_client.py b/discord/voice_client.py index f892bdb9b..6f88856ea 100644 --- a/discord/voice_client.py +++ b/discord/voice_client.py @@ -129,8 +129,7 @@ class VoiceClient: # connection related - @asyncio.coroutine - def start_handshake(self): + async def start_handshake(self): log.info('Starting voice handshake...') key_id, key_name = self.channel._get_voice_client_key() @@ -140,21 +139,20 @@ class VoiceClient: self._connections += 1 # request joining - yield from ws.voice_state(guild_id, channel_id) + await ws.voice_state(guild_id, channel_id) try: - yield from asyncio.wait_for(self._handshake_complete.wait(), timeout=self.timeout, loop=self.loop) + await asyncio.wait_for(self._handshake_complete.wait(), timeout=self.timeout, loop=self.loop) except asyncio.TimeoutError as e: - yield from self.terminate_handshake(remove=True) + await self.terminate_handshake(remove=True) raise e log.info('Voice handshake complete. Endpoint found %s (IP: %s)', self.endpoint, self.endpoint_ip) - @asyncio.coroutine - def terminate_handshake(self, *, remove=False): + async def terminate_handshake(self, *, remove=False): guild_id, channel_id = self.channel._get_voice_state_pair() self._handshake_complete.clear() - yield from self.main_ws.voice_state(guild_id, None, self_mute=True) + await self.main_ws.voice_state(guild_id, None, self_mute=True) log.info('The voice handshake is being terminated for Channel ID %s (Guild ID %s)', channel_id, guild_id) if remove: @@ -162,8 +160,7 @@ class VoiceClient: key_id, _ = self.channel._get_voice_client_key() self._state._remove_voice_client(key_id) - @asyncio.coroutine - def _create_socket(self, server_id, data): + async def _create_socket(self, server_id, data): self._connected.clear() self.session_id = self.main_ws.session_id self.server_id = server_id @@ -190,13 +187,12 @@ class VoiceClient: if self._handshake_complete.is_set(): # terminate the websocket and handle the reconnect loop if necessary. self._handshake_complete.clear() - yield from self.ws.close(4000) + await self.ws.close(4000) return self._handshake_complete.set() - @asyncio.coroutine - def connect(self, *, reconnect=True, _tries=0, do_handshake=True): + async def connect(self, *, reconnect=True, _tries=0, do_handshake=True): log.info('Connecting to voice...') try: del self.secret_key @@ -204,56 +200,54 @@ class VoiceClient: pass if do_handshake: - yield from self.start_handshake() + await self.start_handshake() try: - self.ws = yield from DiscordVoiceWebSocket.from_client(self) + self.ws = await DiscordVoiceWebSocket.from_client(self) self._connected.clear() while not hasattr(self, 'secret_key'): - yield from self.ws.poll_event() + await self.ws.poll_event() self._connected.set() except (ConnectionClosed, asyncio.TimeoutError): if reconnect and _tries < 5: log.exception('Failed to connect to voice... Retrying...') - yield from asyncio.sleep(1 + _tries * 2.0, loop=self.loop) - yield from self.terminate_handshake() - yield from self.connect(reconnect=reconnect, _tries=_tries + 1) + await asyncio.sleep(1 + _tries * 2.0, loop=self.loop) + await self.terminate_handshake() + await self.connect(reconnect=reconnect, _tries=_tries + 1) else: raise if self._runner is None: self._runner = self.loop.create_task(self.poll_voice_ws(reconnect)) - @asyncio.coroutine - def poll_voice_ws(self, reconnect): + async def poll_voice_ws(self, reconnect): backoff = ExponentialBackoff() while True: try: - yield from self.ws.poll_event() + await self.ws.poll_event() except (ConnectionClosed, asyncio.TimeoutError) as e: if isinstance(e, ConnectionClosed): if e.code == 1000: - yield from self.disconnect() + await self.disconnect() break if not reconnect: - yield from self.disconnect() + await self.disconnect() raise e retry = backoff.delay() log.exception('Disconnected from voice... Reconnecting in %.2fs.', retry) self._connected.clear() - yield from asyncio.sleep(retry, loop=self.loop) - yield from self.terminate_handshake() + await asyncio.sleep(retry, loop=self.loop) + await self.terminate_handshake() try: - yield from self.connect(reconnect=True) + await self.connect(reconnect=True) except asyncio.TimeoutError: # at this point we've retried 5 times... let's continue the loop. log.warning('Could not connect to voice... Retrying...') continue - @asyncio.coroutine - def disconnect(self, *, force=False): + async def disconnect(self, *, force=False): """|coro| Disconnects this voice client from voice. @@ -266,15 +260,14 @@ class VoiceClient: try: if self.ws: - yield from self.ws.close() + await self.ws.close() - yield from self.terminate_handshake(remove=True) + await self.terminate_handshake(remove=True) finally: if self.socket: self.socket.close() - @asyncio.coroutine - def move_to(self, channel): + async def move_to(self, channel): """|coro| Moves you to a different voice channel. @@ -285,7 +278,7 @@ class VoiceClient: The channel to move to. Must be a voice channel. """ guild_id, _ = self.channel._get_voice_state_pair() - yield from self.main_ws.voice_state(guild_id, channel.id) + await self.main_ws.voice_state(guild_id, channel.id) def is_connected(self): """:class:`bool`: Indicates if the voice client is connected to voice.""" diff --git a/discord/webhook.py b/discord/webhook.py index d9d380a1d..9d336aee4 100644 --- a/discord/webhook.py +++ b/discord/webhook.py @@ -135,8 +135,7 @@ class AsyncWebhookAdapter(WebhookAdapter): self.session = session self.loop = session.loop - @asyncio.coroutine - def request(self, verb, url, payload=None, multipart=None): + async def request(self, verb, url, payload=None, multipart=None): headers = {} data = None if payload: @@ -152,9 +151,8 @@ class AsyncWebhookAdapter(WebhookAdapter): data.add_field(key, value) for tries in range(5): - r = yield from self.session.request(verb, url, headers=headers, data=data) - try: - data = yield from r.text(encoding='utf-8') + async with self.session.request(verb, url, headers=headers, data=data) as r: + data = await r.text(encoding='utf-8') if r.headers['Content-Type'] == 'application/json': data = json.loads(data) @@ -162,7 +160,7 @@ class AsyncWebhookAdapter(WebhookAdapter): remaining = r.headers.get('X-Ratelimit-Remaining') if remaining == '0' and r.status != 429: delta = utils._parse_ratelimit_header(r) - yield from asyncio.sleep(delta, loop=self.loop) + await asyncio.sleep(delta, loop=self.loop) if 300 > r.status >= 200: return data @@ -170,11 +168,11 @@ class AsyncWebhookAdapter(WebhookAdapter): # we are being rate limited if r.status == 429: retry_after = data['retry_after'] / 1000.0 - yield from asyncio.sleep(retry_after, loop=self.loop) + await asyncio.sleep(retry_after, loop=self.loop) continue if r.status in (500, 502): - yield from asyncio.sleep(1 + tries * 2, loop=self.loop) + await asyncio.sleep(1 + tries * 2, loop=self.loop) continue if r.status == 403: @@ -183,12 +181,9 @@ class AsyncWebhookAdapter(WebhookAdapter): raise NotFound(r, data) else: raise HTTPException(r, data) - finally: - yield from r.release() - @asyncio.coroutine - def handle_execution_response(self, response, *, wait): - data = yield from response + async def handle_execution_response(self, response, *, wait): + data = await response if not wait: return data diff --git a/docs/api.rst b/docs/api.rst index 8be772ac5..109f78f90 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -93,17 +93,8 @@ to handle it, which defaults to print a traceback and ignoring the exception. .. warning:: All the events must be a |corourl|_. If they aren't, then you might get unexpected - errors. In order to turn a function into a coroutine they must either be ``async def`` - functions or in 3.4 decorated with :func:`asyncio.coroutine`. - - The following two functions are examples of coroutine functions: :: - - async def on_ready(): - pass - - @asyncio.coroutine - def on_ready(): - pass + errors. In order to turn a function into a coroutine they must be ``async def`` + functions. .. function:: on_connect() @@ -1306,22 +1297,11 @@ Some API functions return an "async iterator". An async iterator is something th capable of being used in an `async for `_ statement. -These async iterators can be used as follows in 3.5 or higher: :: +These async iterators can be used as follows: :: 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.next() - except discord.NoMoreItems: - break - - # do stuff with item here - Certain utilities make working with async iterators easier, detailed below. .. class:: AsyncIterator diff --git a/docs/faq.rst b/docs/faq.rst index 57daa4719..506a4d43c 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -15,27 +15,6 @@ Coroutines Questions regarding coroutines and asyncio belong here. -I get a SyntaxError around the word ``async``\! What should I do? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This :exc:`SyntaxError` happens because you're using a Python version lower than 3.5. Python 3.4 uses ``@asyncio.coroutine`` and -``yield from`` instead of ``async def`` and ``await``. - -Thus you must do the following instead: :: - - async def foo(): - await bar() - - # into - - @asyncio.coroutine - def foo(): - yield from bar() - -Don't forget to ``import asyncio`` on the top of your files. - -**It is heavily recommended that you update to Python 3.5 or higher as it simplifies asyncio massively.** - What is a coroutine? ~~~~~~~~~~~~~~~~~~~~~~ @@ -195,11 +174,6 @@ technically in another thread, we must take caution in calling thread-safe opera us, :mod:`asyncio` comes with a :func:`asyncio.run_coroutine_threadsafe` function that allows us to call a coroutine from another thread. -.. warning:: - - This function is only part of 3.5.1+ and 3.4.4+. If you are not using these Python versions then use - ``discord.compat.run_coroutine_threadsafe``. - However, this function returns a :class:`concurrent.Future` and to actually call it we have to fetch its result. Putting all of this together we can do the following: :: diff --git a/docs/intro.rst b/docs/intro.rst index 6a8a4d717..29d9f58a4 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -11,9 +11,9 @@ in creating applications that utilise the Discord API. Prerequisites --------------- -discord.py works with Python 3.4.2 or higher. Support for earlier versions of Python -is not provided. Python 2.7 or lower is not supported. Python 3.3 is not supported -due to one of the dependencies (``aiohttp``) not supporting Python 3.3. +discord.py works with Python 3.5.2 or higher. Support for earlier versions of Python +is not provided. Python 2.7 or lower is not supported. Python 3.4 or lower is not supported +due to one of the dependencies (``aiohttp``) not supporting Python 3.4. .. _installing: diff --git a/docs/migrating.rst b/docs/migrating.rst index e9fbf0005..6d196d875 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -14,6 +14,13 @@ new library. Part of the redesign involves making things more easy to use and natural. Things are done on the :ref:`models ` instead of requiring a :class:`Client` instance to do any work. +Python Version Change +----------------------- + +In order to make development easier and also to allow for our dependencies to upgrade to allow usage of 3.7 or higher, +the library had to remove support for Python versions lower than 3.5.2, which essentially means that **support for Python 3.4 +is dropped**. + Major Model Changes --------------------- @@ -441,14 +448,14 @@ Prior to v1.0, certain functions like ``Client.logs_from`` would return a differ In v1.0, this change has been reverted and will now return a singular type meeting an abstract concept called :class:`AsyncIterator`. -This allows you to iterate over it like normal in Python 3.5+: :: +This allows you to iterate over it like normal: :: async for message in channel.history(): print(message) -Or turn it into a list for either Python 3.4 or 3.5+: :: +Or turn it into a list: :: - messages = await channel.history().flatten() # use yield from for 3.4! + messages = await channel.history().flatten() for message in messages: print(message) diff --git a/requirements.txt b/requirements.txt index 867a40634..607d4503d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -aiohttp>=2.0.0,<2.3.0 -websockets>=3.0,<4.0 +aiohttp>=3.3.0,<3.4.0 +websockets>=5.0,<6.0 diff --git a/setup.py b/setup.py index b2c74039d..bfc110c12 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,6 @@ setup(name='discord.py', 'Intended Audience :: Developers', 'Natural Language :: English', 'Operating System :: OS Independent', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Internet',