Browse Source

Drop support for Python 3.4 and make minimum version 3.5.2.

pull/1343/head
Rapptz 7 years ago
parent
commit
f25091efe1
  1. 4
      README.rst
  2. 98
      discord/abc.py
  3. 85
      discord/channel.py
  4. 128
      discord/client.py
  5. 140
      discord/compat.py
  6. 19
      discord/context_managers.py
  7. 10
      discord/emoji.py
  8. 4
      discord/errors.py
  9. 72
      discord/ext/commands/bot.py
  10. 13
      discord/ext/commands/context.py
  11. 41
      discord/ext/commands/converter.py
  12. 140
      discord/ext/commands/core.py
  13. 20
      discord/ext/commands/formatter.py
  14. 152
      discord/gateway.py
  15. 89
      discord/guild.py
  16. 63
      discord/http.py
  17. 5
      discord/invite.py
  18. 126
      discord/iterators.py
  19. 46
      discord/member.py
  20. 51
      discord/message.py
  21. 2
      discord/reaction.py
  22. 10
      discord/relationship.py
  23. 17
      discord/role.py
  24. 91
      discord/shard.py
  25. 47
      discord/state.py
  26. 45
      discord/user.py
  27. 19
      discord/utils.py
  28. 61
      discord/voice_client.py
  29. 21
      discord/webhook.py
  30. 26
      docs/api.rst
  31. 26
      docs/faq.rst
  32. 6
      docs/intro.rst
  33. 13
      docs/migrating.rst
  34. 4
      requirements.txt
  35. 1
      setup.py

4
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)

98
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

85
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)

128
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 <discord-api-events>`.
"""
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)

140
discord/compat.py

@ -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

19
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()

10
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)

4
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):

72
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.

13
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

41
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'<a?:[a-zA-Z0-9\_]+:([0-9]+)>$', 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 = {}

140
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

20
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):

152
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('<H', recv, len(recv) - 2)[0]
log.debug('detected ip: %s port: %s', state.ip, state.port)
yield from self.select_protocol(state.ip, state.port)
await self.select_protocol(state.ip, state.port)
log.info('selected the voice protocol for use')
@asyncio.coroutine
def load_secret_key(self, data):
async def load_secret_key(self, data):
log.info('received secret key for voice connection')
self._connection.secret_key = data.get('secret_key')
yield from self.speak()
await self.speak()
@asyncio.coroutine
def poll_event(self):
async def poll_event(self):
try:
msg = yield from asyncio.wait_for(self.recv(), timeout=30.0, loop=self.loop)
yield from self.received_message(json.loads(msg))
msg = await asyncio.wait_for(self.recv(), timeout=30.0, loop=self.loop)
await self.received_message(json.loads(msg))
except websockets.exceptions.ConnectionClosed as e:
raise ConnectionClosed(e, shard_id=None) from e
@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)

89
discord/guild.py

@ -543,8 +543,7 @@ class Guild(Hashable):
return self._state.http.create_channel(self.id, name, channel_type.value, parent_id=parent_id,
permission_overwrites=perms, reason=reason)
@asyncio.coroutine
def create_text_channel(self, name, *, overwrites=None, category=None, reason=None):
async def create_text_channel(self, name, *, overwrites=None, category=None, reason=None):
"""|coro|
Creates a :class:`TextChannel` for the guild.
@ -606,28 +605,26 @@ class Guild(Hashable):
:class:`TextChannel`
The channel that was just created.
"""
data = yield from self._create_channel(name, overwrites, ChannelType.text, category, reason=reason)
data = await self._create_channel(name, overwrites, ChannelType.text, category, reason=reason)
channel = TextChannel(state=self._state, guild=self, data=data)
# temporarily add to the cache
self._channels[channel.id] = channel
return channel
@asyncio.coroutine
def create_voice_channel(self, name, *, overwrites=None, category=None, reason=None):
async def create_voice_channel(self, name, *, overwrites=None, category=None, reason=None):
"""|coro|
Same as :meth:`create_text_channel` except makes a :class:`VoiceChannel` instead.
"""
data = yield from self._create_channel(name, overwrites, ChannelType.voice, category, reason=reason)
data = await self._create_channel(name, overwrites, ChannelType.voice, category, reason=reason)
channel = VoiceChannel(state=self._state, guild=self, data=data)
# temporarily add to the cache
self._channels[channel.id] = channel
return channel
@asyncio.coroutine
def create_category(self, name, *, overwrites=None, reason=None):
async def create_category(self, name, *, overwrites=None, reason=None):
"""|coro|
Same as :meth:`create_text_channel` except makes a :class:`CategoryChannel` instead.
@ -637,7 +634,7 @@ class Guild(Hashable):
The ``category`` parameter is not supported in this function since categories
cannot have categories.
"""
data = yield from self._create_channel(name, overwrites, ChannelType.category, reason=reason)
data = await self._create_channel(name, overwrites, ChannelType.category, reason=reason)
channel = CategoryChannel(state=self._state, guild=self, data=data)
# temporarily add to the cache
@ -646,8 +643,7 @@ class Guild(Hashable):
create_category_channel = create_category
@asyncio.coroutine
def leave(self):
async def leave(self):
"""|coro|
Leaves the guild.
@ -662,10 +658,9 @@ class Guild(Hashable):
HTTPException
Leaving the guild failed.
"""
yield from self._state.http.leave_guild(self.id)
await self._state.http.leave_guild(self.id)
@asyncio.coroutine
def delete(self):
async def delete(self):
"""|coro|
Deletes the guild. You must be the guild owner to delete the
@ -679,10 +674,9 @@ class Guild(Hashable):
You do not have permissions to delete the guild.
"""
yield from self._state.http.delete_guild(self.id)
await self._state.http.delete_guild(self.id)
@asyncio.coroutine
def edit(self, *, reason=None, **fields):
async def edit(self, *, reason=None, **fields):
"""|coro|
Edits the guild.
@ -748,7 +742,7 @@ class Guild(Hashable):
except KeyError:
pass
else:
yield from http.change_vanity_code(self.id, vanity_code, reason=reason)
await http.change_vanity_code(self.id, vanity_code, reason=reason)
try:
splash_bytes = fields['splash']
@ -798,7 +792,7 @@ class Guild(Hashable):
fields['verification_level'] = level.value
yield from http.edit_guild(self.id, reason=reason, **fields)
await http.edit_guild(self.id, reason=reason, **fields)
@asyncio.coroutine
def get_ban(self, user):
@ -836,8 +830,7 @@ class Guild(Hashable):
reason=data['reason']
)
@asyncio.coroutine
def bans(self):
async def bans(self):
"""|coro|
Retrieves all the users that are banned from the guild.
@ -863,13 +856,12 @@ class Guild(Hashable):
A list of BanEntry objects.
"""
data = yield from self._state.http.get_bans(self.id)
data = await self._state.http.get_bans(self.id)
return [BanEntry(user=User(state=self._state, data=e['user']),
reason=e['reason'])
for e in data]
@asyncio.coroutine
def prune_members(self, *, days, reason=None):
async def prune_members(self, *, days, reason=None):
"""|coro|
Prunes the guild from its inactive members.
@ -908,11 +900,10 @@ class Guild(Hashable):
if not isinstance(days, int):
raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days))
data = yield from self._state.http.prune_members(self.id, days, reason=reason)
data = await self._state.http.prune_members(self.id, days, reason=reason)
return data['pruned']
@asyncio.coroutine
def webhooks(self):
async def webhooks(self):
"""|coro|
Gets the list of webhooks from this guild.
@ -930,11 +921,10 @@ class Guild(Hashable):
The webhooks for this guild.
"""
data = yield from self._state.http.guild_webhooks(self.id)
data = await self._state.http.guild_webhooks(self.id)
return [Webhook.from_state(d, state=self._state) for d in data]
@asyncio.coroutine
def estimate_pruned_members(self, *, days):
async def estimate_pruned_members(self, *, days):
"""|coro|
Similar to :meth:`prune_members` except instead of actually
@ -964,11 +954,10 @@ class Guild(Hashable):
if not isinstance(days, int):
raise InvalidArgument('Expected int for ``days``, received {0.__class__.__name__} instead.'.format(days))
data = yield from self._state.http.estimate_pruned_members(self.id, days)
data = await self._state.http.estimate_pruned_members(self.id, days)
return data['pruned']
@asyncio.coroutine
def invites(self):
async def invites(self):
"""|coro|
Returns a list of all active instant invites from the guild.
@ -989,7 +978,7 @@ class Guild(Hashable):
The list of invites that are currently active.
"""
data = yield from self._state.http.invites_from(self.id)
data = await self._state.http.invites_from(self.id)
result = []
for invite in data:
channel = self.get_channel(int(invite['channel']['id']))
@ -999,8 +988,7 @@ class Guild(Hashable):
return result
@asyncio.coroutine
def create_custom_emoji(self, *, name, image, reason=None):
async def create_custom_emoji(self, *, name, image, reason=None):
"""|coro|
Creates a custom :class:`Emoji` for the guild.
@ -1036,11 +1024,10 @@ class Guild(Hashable):
"""
img = utils._bytes_to_base64_data(image)
data = yield from self._state.http.create_custom_emoji(self.id, name, img, reason=reason)
data = await self._state.http.create_custom_emoji(self.id, name, img, reason=reason)
return self._state.store_emoji(self, data)
@asyncio.coroutine
def create_role(self, *, reason=None, **fields):
async def create_role(self, *, reason=None, **fields):
"""|coro|
Creates a :class:`Role` for the guild.
@ -1102,14 +1089,13 @@ class Guild(Hashable):
if key not in valid_keys:
raise InvalidArgument('%r is not a valid field.' % key)
data = yield from self._state.http.create_role(self.id, reason=reason, **fields)
data = await self._state.http.create_role(self.id, reason=reason, **fields)
role = Role(guild=self, data=data, state=self._state)
# TODO: add to cache
return role
@asyncio.coroutine
def kick(self, user, *, reason=None):
async def kick(self, user, *, reason=None):
"""|coro|
Kicks a user from the guild.
@ -1133,10 +1119,9 @@ class Guild(Hashable):
HTTPException
Kicking failed.
"""
yield from self._state.http.kick(user.id, self.id, reason=reason)
await self._state.http.kick(user.id, self.id, reason=reason)
@asyncio.coroutine
def ban(self, user, *, reason=None, delete_message_days=1):
async def ban(self, user, *, reason=None, delete_message_days=1):
"""|coro|
Bans a user from the guild.
@ -1163,10 +1148,9 @@ class Guild(Hashable):
HTTPException
Banning failed.
"""
yield from self._state.http.ban(user.id, self.id, delete_message_days, reason=reason)
await self._state.http.ban(user.id, self.id, delete_message_days, reason=reason)
@asyncio.coroutine
def unban(self, user, *, reason=None):
async def unban(self, user, *, reason=None):
"""|coro|
Unbans a user from the guild.
@ -1190,10 +1174,9 @@ class Guild(Hashable):
HTTPException
Unbanning failed.
"""
yield from self._state.http.unban(user.id, self.id, reason=reason)
await self._state.http.unban(user.id, self.id, reason=reason)
@asyncio.coroutine
def vanity_invite(self):
async def vanity_invite(self):
"""|coro|
Returns the guild's special vanity invite.
@ -1218,11 +1201,11 @@ class Guild(Hashable):
"""
# we start with { code: abc }
payload = yield from self._state.http.get_vanity_code(self.id)
payload = await self._state.http.get_vanity_code(self.id)
# get the vanity URL channel since default channels aren't
# reliable or a thing anymore
data = yield from self._state.http.get_invite(payload['code'])
data = await self._state.http.get_invite(payload['code'])
payload['guild'] = self
payload['channel'] = self.get_channel(int(data['channel']['id']))

63
discord/http.py

@ -38,9 +38,8 @@ log = logging.getLogger(__name__)
from .errors import HTTPException, Forbidden, NotFound, LoginFailure, GatewayNotFound
from . import __version__, utils
@asyncio.coroutine
def json_or_text(response):
text = yield from response.text(encoding='utf-8')
async def json_or_text(response):
text = await response.text(encoding='utf-8')
if response.headers['content-type'] == 'application/json':
return json.loads(text)
return text
@ -106,8 +105,7 @@ class HTTPClient:
if self._session.closed:
self._session = aiohttp.ClientSession(connector=self.connector, loop=self.loop)
@asyncio.coroutine
def request(self, route, *, header_bypass_delay=None, **kwargs):
async def request(self, route, *, header_bypass_delay=None, **kwargs):
bucket = route.bucket
method = route.method
url = route.url
@ -148,16 +146,16 @@ class HTTPClient:
if not self._global_over.is_set():
# wait until the global lock is complete
yield from self._global_over.wait()
await self._global_over.wait()
yield from lock
await lock
with MaybeUnlock(lock) as maybe_lock:
for tries in range(5):
r = yield from self._session.request(method, url, **kwargs)
log.debug('%s %s with %s has returned %s', method, url, kwargs.get('data'), r.status)
try:
async with self._session.request(method, url, **kwargs) as r:
log.debug('%s %s with %s has returned %s', method, url, kwargs.get('data'), r.status)
# even errors have text involved in them so this is safe to call
data = yield from json_or_text(r)
data = await json_or_text(r)
# check if we have rate limit header information
remaining = r.headers.get('X-Ratelimit-Remaining')
@ -191,7 +189,7 @@ class HTTPClient:
log.info('Global rate limit has been hit. Retrying in %.2f seconds.', retry_after)
self._global_over.clear()
yield from asyncio.sleep(retry_after, loop=self.loop)
await asyncio.sleep(retry_after, loop=self.loop)
log.debug('Done sleeping for the rate limit. Retrying...')
# release the global lock now that the
@ -204,7 +202,7 @@ class HTTPClient:
# we've received a 500 or 502, unconditional retry
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
# the usual error cases
@ -214,32 +212,25 @@ class HTTPClient:
raise NotFound(r, data)
else:
raise HTTPException(r, data)
finally:
# clean-up just in case
yield from r.release()
# We've run out of retries, raise.
raise HTTPException(r, data)
@asyncio.coroutine
def get_attachment(self, url):
resp = yield from self._session.get(url)
try:
async def get_attachment(self, url):
async with self._session.get(url) as resp:
if resp.status == 200:
return (yield from resp.read())
return (await resp.read())
elif resp.status == 404:
raise NotFound(resp, 'attachment not found')
elif resp.status == 403:
raise Forbidden(resp, 'cannot retrieve attachment')
else:
raise HTTPException(resp, 'failed to get attachment')
finally:
yield from resp.release()
# state management
@asyncio.coroutine
def close(self):
yield from self._session.close()
async def close(self):
await self._session.close()
def _token(self, token, *, bot=True):
self.token = token
@ -248,13 +239,12 @@ class HTTPClient:
# login management
@asyncio.coroutine
def static_login(self, token, *, bot):
async def static_login(self, token, *, bot):
old_token, old_bot = self.token, self.bot_token
self._token(token, bot=bot)
try:
data = yield from self.request(Route('GET', '/users/@me'))
data = await self.request(Route('GET', '/users/@me'))
except HTTPException as e:
self._token(old_token, bot=old_bot)
if e.response.status == 401:
@ -349,11 +339,10 @@ class HTTPClient:
return self.request(r, data=form)
@asyncio.coroutine
def ack_message(self, channel_id, message_id):
async def ack_message(self, channel_id, message_id):
r = Route('POST', '/channels/{channel_id}/messages/{message_id}/ack', channel_id=channel_id,
message_id=message_id)
data = yield from self.request(r, json={'token': self._ack_token})
data = await self.request(r, json={'token': self._ack_token})
self._ack_token = data['token']
def ack_guild(self, guild_id):
@ -751,10 +740,9 @@ class HTTPClient:
def application_info(self):
return self.request(Route('GET', '/oauth2/applications/@me'))
@asyncio.coroutine
def get_gateway(self, *, encoding='json', v=6, zlib=True):
async def get_gateway(self, *, encoding='json', v=6, zlib=True):
try:
data = yield from self.request(Route('GET', '/gateway'))
data = await self.request(Route('GET', '/gateway'))
except HTTPException as e:
raise GatewayNotFound() from e
if zlib:
@ -763,10 +751,9 @@ class HTTPClient:
value = '{0}?encoding={1}&v={2}'
return value.format(data['url'], encoding, v)
@asyncio.coroutine
def get_bot_gateway(self, *, encoding='json', v=6, zlib=True):
async def get_bot_gateway(self, *, encoding='json', v=6, zlib=True):
try:
data = yield from self.request(Route('GET', '/gateway/bot'))
data = await self.request(Route('GET', '/gateway/bot'))
except HTTPException as e:
raise GatewayNotFound() from e

5
discord/invite.py

@ -134,8 +134,7 @@ class Invite(Hashable):
"""A property that retrieves the invite URL."""
return 'http://discord.gg/' + self.code
@asyncio.coroutine
def delete(self, *, reason=None):
async def delete(self, *, reason=None):
"""|coro|
Revokes the instant invite.
@ -157,4 +156,4 @@ class Invite(Hashable):
Revoking the invite failed.
"""
yield from self._state.http.delete_invite(self.code, reason=reason)
await self._state.http.delete_invite(self.code, reason=reason)

126
discord/iterators.py

@ -33,8 +33,6 @@ from .utils import time_snowflake, maybe_coroutine
from .object import Object
from .audit_logs import AuditLogEntry
PY35 = sys.version_info >= (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))

46
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)

51
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|

2
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:

10
discord/relationship.py

@ -52,8 +52,7 @@ class Relationship:
def __repr__(self):
return '<Relationship user={0.user!r} type={0.type!r}>'.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)

17
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)

91
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:

47
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)

45
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 '<User id={0.id} name={0.name!r} discriminator={0.discriminator!r} bot={0.bot}>'.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']))

19
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()

61
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."""

21
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

26
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 <https://docs.python.org/3/reference/compound_stmts.html#the-async-for-statement>`_
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

26
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: ::

6
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:

13
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 <discord_api_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)

4
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

1
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',

Loading…
Cancel
Save