Browse Source

Convert datetimes to aware datetimes with UTC.

Naive datetimes will now be interpreted as local time throughout
the library.
pull/6632/head
Rapptz 4 years ago
parent
commit
ff7094ce96
  1. 11
      discord/abc.py
  2. 28
      discord/activity.py
  3. 6
      discord/client.py
  4. 6
      discord/embeds.py
  5. 9
      discord/guild.py
  6. 4
      discord/integrations.py
  7. 2
      discord/invite.py
  8. 6
      discord/member.py
  9. 7
      discord/message.py
  10. 1
      discord/state.py
  11. 2
      discord/sticker.py
  12. 5
      discord/template.py
  13. 3
      discord/user.py
  14. 48
      discord/utils.py
  15. 6
      docs/api.rst

11
discord/abc.py

@ -75,7 +75,7 @@ class Snowflake(Protocol):
@property @property
def created_at(self) -> datetime: def created_at(self) -> datetime:
""":class:`datetime.datetime`: Returns the model's creation time as a naive datetime in UTC.""" """:class:`datetime.datetime`: Returns the model's creation time as an aware datetime in UTC."""
raise NotImplementedError raise NotImplementedError
@ -1176,13 +1176,16 @@ class Messageable(Protocol):
that this would make it a slow operation. that this would make it a slow operation.
before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve messages before this date or message. Retrieve messages before this date or message.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
after: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] after: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve messages after this date or message. Retrieve messages after this date or message.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
around: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] around: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve messages around this date or message. Retrieve messages around this date or message.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
When using this argument, the maximum limit is 101. Note that if the limit is an When using this argument, the maximum limit is 101. Note that if the limit is an
even number then this will return at most limit + 1 messages. even number then this will return at most limit + 1 messages.
oldest_first: Optional[:class:`bool`] oldest_first: Optional[:class:`bool`]

28
discord/activity.py

@ -225,17 +225,21 @@ class Activity(BaseActivity):
def start(self): def start(self):
"""Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable.""" """Optional[:class:`datetime.datetime`]: When the user started doing this activity in UTC, if applicable."""
try: try:
return datetime.datetime.utcfromtimestamp(self.timestamps['start'] / 1000) timestamp = self.timestamps['start'] / 1000
except KeyError: except KeyError:
return None return None
else:
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
@property @property
def end(self): def end(self):
"""Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable.""" """Optional[:class:`datetime.datetime`]: When the user will stop doing this activity in UTC, if applicable."""
try: try:
return datetime.datetime.utcfromtimestamp(self.timestamps['end'] / 1000) timestamp = self.timestamps['end'] / 1000
except KeyError: except KeyError:
return None return None
else:
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
@property @property
def large_image_url(self): def large_image_url(self):
@ -300,10 +304,6 @@ class Game(BaseActivity):
----------- -----------
name: :class:`str` name: :class:`str`
The game's name. The game's name.
start: Optional[:class:`datetime.datetime`]
A naive UTC timestamp representing when the game started. Keyword-only parameter. Ignored for bots.
end: Optional[:class:`datetime.datetime`]
A naive UTC timestamp representing when the game ends. Keyword-only parameter. Ignored for bots.
Attributes Attributes
----------- -----------
@ -320,20 +320,12 @@ class Game(BaseActivity):
try: try:
timestamps = extra['timestamps'] timestamps = extra['timestamps']
except KeyError: except KeyError:
self._extract_timestamp(extra, 'start') self._start = 0
self._extract_timestamp(extra, 'end') self._end = 0
else: else:
self._start = timestamps.get('start', 0) self._start = timestamps.get('start', 0)
self._end = timestamps.get('end', 0) self._end = timestamps.get('end', 0)
def _extract_timestamp(self, data, key):
try:
dt = data[key]
except KeyError:
setattr(self, '_' + key, 0)
else:
setattr(self, '_' + key, dt.timestamp() * 1000.0)
@property @property
def type(self): def type(self):
""":class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`. """:class:`ActivityType`: Returns the game's type. This is for compatibility with :class:`Activity`.
@ -346,14 +338,14 @@ class Game(BaseActivity):
def start(self): def start(self):
"""Optional[:class:`datetime.datetime`]: When the user started playing this game in UTC, if applicable.""" """Optional[:class:`datetime.datetime`]: When the user started playing this game in UTC, if applicable."""
if self._start: if self._start:
return datetime.datetime.utcfromtimestamp(self._start / 1000) return datetime.datetime.utcfromtimestamp(self._start / 1000).replace(tzinfo=datetime.timezone.utc)
return None return None
@property @property
def end(self): def end(self):
"""Optional[:class:`datetime.datetime`]: When the user will stop playing this game in UTC, if applicable.""" """Optional[:class:`datetime.datetime`]: When the user will stop playing this game in UTC, if applicable."""
if self._end: if self._end:
return datetime.datetime.utcfromtimestamp(self._end / 1000) return datetime.datetime.utcfromtimestamp(self._end / 1000).replace(tzinfo=datetime.timezone.utc)
return None return None
def __str__(self): def __str__(self):

6
discord/client.py

@ -1076,10 +1076,12 @@ class Client:
Defaults to ``100``. Defaults to ``100``.
before: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`] before: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]
Retrieves guilds before this date or object. Retrieves guilds before this date or object.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
after: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`] after: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]
Retrieve guilds after this date or object. Retrieve guilds after this date or object.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
Raises Raises
------ ------

6
discord/embeds.py

@ -87,7 +87,9 @@ class Embed:
The URL of the embed. The URL of the embed.
This can be set during initialisation. This can be set during initialisation.
timestamp: :class:`datetime.datetime` timestamp: :class:`datetime.datetime`
The timestamp of the embed content. This could be a naive or aware datetime. The timestamp of the embed content. This is an aware datetime.
If a naive datetime is passed, it is converted to an aware
datetime with the local timezone.
colour: Union[:class:`Colour`, :class:`int`] colour: Union[:class:`Colour`, :class:`int`]
The colour code of the embed. Aliased to ``color`` as well. The colour code of the embed. Aliased to ``color`` as well.
This can be set during initialisation. This can be set during initialisation.
@ -129,6 +131,8 @@ class Embed:
except KeyError: except KeyError:
pass pass
else: else:
if timestamp.tzinfo is None:
timestamp = timestamp.astimezone()
self.timestamp = timestamp self.timestamp = timestamp
@classmethod @classmethod

9
discord/guild.py

@ -1336,7 +1336,8 @@ class Guild(Hashable):
Pass ``None`` to fetch all members. Note that this is potentially slow. Pass ``None`` to fetch all members. Note that this is potentially slow.
after: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]] after: Optional[Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`]]
Retrieve members after this date or object. Retrieve members after this date or object.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
Raises Raises
------ ------
@ -2119,10 +2120,12 @@ class Guild(Hashable):
The number of entries to retrieve. If ``None`` retrieve all entries. The number of entries to retrieve. If ``None`` retrieve all entries.
before: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`] before: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]
Retrieve entries before this date or entry. Retrieve entries before this date or entry.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
after: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`] after: Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]
Retrieve entries after this date or entry. Retrieve entries after this date or entry.
If a date is provided it must be a timezone-naive datetime representing UTC time. If a datetime is provided, it is recommended to use a UTC aware datetime.
If the datetime is naive, it is assumed to be local time.
oldest_first: :class:`bool` oldest_first: :class:`bool`
If set to ``True``, return entries in oldest->newest order. Defaults to ``True`` if If set to ``True``, return entries in oldest->newest order. Defaults to ``True`` if
``after`` is specified, otherwise ``False``. ``after`` is specified, otherwise ``False``.

4
discord/integrations.py

@ -82,7 +82,7 @@ class Integration:
account: :class:`IntegrationAccount` account: :class:`IntegrationAccount`
The integration account information. The integration account information.
synced_at: :class:`datetime.datetime` synced_at: :class:`datetime.datetime`
When the integration was last synced. An aware UTC datetime representing when the integration was last synced.
""" """
__slots__ = ('id', '_state', 'guild', 'name', 'enabled', 'type', __slots__ = ('id', '_state', 'guild', 'name', 'enabled', 'type',
@ -184,7 +184,7 @@ class Integration:
Syncing the integration failed. Syncing the integration failed.
""" """
await self._state.http.sync_integration(self.guild.id, self.id) await self._state.http.sync_integration(self.guild.id, self.id)
self.synced_at = datetime.datetime.utcnow() self.synced_at = datetime.datetime.now(datetime.timezone.utc)
async def delete(self): async def delete(self):
"""|coro| """|coro|

2
discord/invite.py

@ -265,7 +265,7 @@ class Invite(Hashable):
revoked: :class:`bool` revoked: :class:`bool`
Indicates if the invite has been revoked. Indicates if the invite has been revoked.
created_at: :class:`datetime.datetime` created_at: :class:`datetime.datetime`
A datetime object denoting the time the invite was created. An aware UTC datetime object denoting the time the invite was created.
temporary: :class:`bool` temporary: :class:`bool`
Indicates that the invite grants temporary membership. Indicates that the invite grants temporary membership.
If ``True``, members who joined via this invite will be kicked upon disconnect. If ``True``, members who joined via this invite will be kicked upon disconnect.

6
discord/member.py

@ -67,7 +67,7 @@ class VoiceState:
.. versionadded:: 1.7 .. versionadded:: 1.7
requested_to_speak_at: Optional[:class:`datetime.datetime`] requested_to_speak_at: Optional[:class:`datetime.datetime`]
A datetime object that specifies the date and time in UTC that the member An aware datetime object that specifies the date and time in UTC that the member
requested to speak. It will be ``None`` if they are not requesting to speak requested to speak. It will be ``None`` if they are not requesting to speak
anymore or have been accepted to speak. anymore or have been accepted to speak.
@ -183,7 +183,7 @@ class Member(discord.abc.Messageable, _BaseUser):
Attributes Attributes
---------- ----------
joined_at: Optional[:class:`datetime.datetime`] joined_at: Optional[:class:`datetime.datetime`]
A datetime object that specifies the date and time in UTC that the member joined the guild. An aware datetime object that specifies the date and time in UTC that the member joined the guild.
If the member left and rejoined the guild, this will be the latest date. In certain cases, this can be ``None``. If the member left and rejoined the guild, this will be the latest date. In certain cases, this can be ``None``.
activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]] activities: Tuple[Union[:class:`BaseActivity`, :class:`Spotify`]]
The activities that the user is currently doing. The activities that the user is currently doing.
@ -196,7 +196,7 @@ class Member(discord.abc.Messageable, _BaseUser):
.. versionadded:: 1.6 .. versionadded:: 1.6
premium_since: Optional[:class:`datetime.datetime`] premium_since: Optional[:class:`datetime.datetime`]
A datetime object that specifies the date and time in UTC when the member used their An aware datetime object that specifies the date and time in UTC when the member used their
Nitro boost on the guild, if available. This could be ``None``. Nitro boost on the guild, if available. This could be ``None``.
""" """

7
discord/message.py

@ -842,7 +842,7 @@ class Message(Hashable):
@property @property
def edited_at(self): def edited_at(self):
"""Optional[:class:`datetime.datetime`]: A naive UTC datetime object containing the edited time of the message.""" """Optional[:class:`datetime.datetime`]: An aware UTC datetime object containing the edited time of the message."""
return self._edited_timestamp return self._edited_timestamp
@property @property
@ -903,10 +903,7 @@ class Message(Hashable):
"Yay you made it, {0}!", "Yay you made it, {0}!",
] ]
# manually reconstruct the epoch with millisecond precision, because created_at_ms = int(self.created_at.timestamp() * 1000)
# datetime.datetime.timestamp() doesn't return the exact posix
# timestamp with the precision that we need
created_at_ms = int((self.created_at - datetime.datetime(1970, 1, 1)).total_seconds() * 1000)
return formats[created_at_ms % len(formats)].format(self.author.name) return formats[created_at_ms % len(formats)].format(self.author.name)
if self.type is MessageType.premium_guild_subscription: if self.type is MessageType.premium_guild_subscription:

1
discord/state.py

@ -1013,6 +1013,7 @@ class ConnectionState:
if member is not None: if member is not None:
timestamp = datetime.datetime.utcfromtimestamp(data.get('timestamp')) timestamp = datetime.datetime.utcfromtimestamp(data.get('timestamp'))
timestamp = timestamp.replace(tzinfo=datetime.timezone.utc)
self.dispatch('typing', channel, member, timestamp) self.dispatch('typing', channel, member, timestamp)
def _get_reaction_user(self, channel, user_id): def _get_reaction_user(self, channel, user_id):

2
discord/sticker.py

@ -91,7 +91,7 @@ class Sticker(Hashable):
@property @property
def created_at(self): def created_at(self):
""":class:`datetime.datetime`: Returns the sticker's creation time in UTC as a naive datetime.""" """:class:`datetime.datetime`: Returns the sticker's creation time in UTC."""
return snowflake_time(self.id) return snowflake_time(self.id)
@property @property

5
discord/template.py

@ -90,9 +90,10 @@ class Template:
creator: :class:`User` creator: :class:`User`
The creator of the template. The creator of the template.
created_at: :class:`datetime.datetime` created_at: :class:`datetime.datetime`
When the template was created. An aware datetime in UTC representing when the template was created.
updated_at: :class:`datetime.datetime` updated_at: :class:`datetime.datetime`
When the template was last updated (referred to as "last synced" in the client). An aware datetime in UTC representing when the template was last updated.
This is referred to as "last synced" in the official Discord client.
source_guild: :class:`Guild` source_guild: :class:`Guild`
The source guild. The source guild.
""" """

3
discord/user.py

@ -192,7 +192,8 @@ class BaseUser(_BaseUser):
def created_at(self): def created_at(self):
""":class:`datetime.datetime`: Returns the user's creation time in UTC. """:class:`datetime.datetime`: Returns the user's creation time in UTC.
This is when the user's Discord account was created.""" This is when the user's Discord account was created.
"""
return snowflake_time(self.id) return snowflake_time(self.id)
@property @property

48
discord/utils.py

@ -25,6 +25,7 @@ DEALINGS IN THE SOFTWARE.
import array import array
import asyncio import asyncio
import collections.abc import collections.abc
from typing import Optional, overload
import unicodedata import unicodedata
from base64 import b64encode from base64 import b64encode
from bisect import bisect_left from bisect import bisect_left
@ -103,9 +104,17 @@ class SequenceProxy(collections.abc.Sequence):
def count(self, value): def count(self, value):
return self.__proxied.count(value) return self.__proxied.count(value)
def parse_time(timestamp): @overload
def parse_time(timestamp: None) -> None:
...
@overload
def parse_time(timestamp: str) -> datetime.datetime:
...
def parse_time(timestamp: Optional[str]) -> Optional[datetime.datetime]:
if timestamp: if timestamp:
return datetime.datetime(*map(int, re.split(r'[^\d]', timestamp.replace('+00:00', '')))) return datetime.datetime.fromisoformat(timestamp)
return None return None
def copy_doc(original): def copy_doc(original):
@ -168,7 +177,7 @@ def oauth_url(client_id, permissions=None, guild=None, redirect_uri=None, scopes
return url return url
def snowflake_time(id): def snowflake_time(id: int) -> datetime.datetime:
""" """
Parameters Parameters
----------- -----------
@ -178,25 +187,34 @@ def snowflake_time(id):
Returns Returns
-------- --------
:class:`datetime.datetime` :class:`datetime.datetime`
The creation date in UTC of a Discord snowflake ID.""" An aware datetime in UTC representing the creation time of the snowflake.
return datetime.datetime.utcfromtimestamp(((id >> 22) + DISCORD_EPOCH) / 1000) """
timestamp = ((id >> 22) + DISCORD_EPOCH) / 1000
return datetime.datetime.utcfromtimestamp(timestamp).replace(tzinfo=datetime.timezone.utc)
def time_snowflake(datetime_obj, high=False): def time_snowflake(dt: datetime.datetime, high: bool = False) -> int:
"""Returns a numeric snowflake pretending to be created at the given date. """Returns a numeric snowflake pretending to be created at the given date.
When using as the lower end of a range, use ``time_snowflake(high=False) - 1`` to be inclusive, ``high=True`` to be exclusive When using as the lower end of a range, use ``time_snowflake(high=False) - 1``
When using as the higher end of a range, use ``time_snowflake(high=True)`` + 1 to be inclusive, ``high=False`` to be exclusive to be inclusive, ``high=True`` to be exclusive.
When using as the higher end of a range, use ``time_snowflake(high=True) + 1``
to be inclusive, ``high=False`` to be exclusive
Parameters Parameters
----------- -----------
datetime_obj: :class:`datetime.datetime` dt: :class:`datetime.datetime`
A timezone-naive datetime object representing UTC time. A datetime object to convert to a snowflake.
If naive, the timezone is assumed to be local time.
high: :class:`bool` high: :class:`bool`
Whether or not to set the lower 22 bit to high or low. Whether or not to set the lower 22 bit to high or low.
"""
unix_seconds = (datetime_obj - type(datetime_obj)(1970, 1, 1)).total_seconds()
discord_millis = int(unix_seconds * 1000 - DISCORD_EPOCH)
Returns
--------
:class:`int`
The snowflake representing the time given.
"""
discord_millis = int(dt.timestamp() * 1000 - DISCORD_EPOCH)
return (discord_millis << 22) + (2**22-1 if high else 0) return (discord_millis << 22) + (2**22-1 if high else 0)
def find(predicate, seq): def find(predicate, seq):
@ -374,12 +392,12 @@ async def sleep_until(when, result=None):
----------- -----------
when: :class:`datetime.datetime` when: :class:`datetime.datetime`
The timestamp in which to sleep until. If the datetime is naive then The timestamp in which to sleep until. If the datetime is naive then
it is assumed to be in UTC. it is assumed to be local time.
result: Any result: Any
If provided is returned to the caller when the coroutine completes. If provided is returned to the caller when the coroutine completes.
""" """
if when.tzinfo is None: if when.tzinfo is None:
when = when.replace(tzinfo=datetime.timezone.utc) when = when.astimezone()
now = datetime.datetime.now(datetime.timezone.utc) now = datetime.datetime.now(datetime.timezone.utc)
delta = (when - now).total_seconds() delta = (when - now).total_seconds()
while delta > MAX_ASYNCIO_SECONDS: while delta > MAX_ASYNCIO_SECONDS:

6
docs/api.rst

@ -340,7 +340,7 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:type channel: :class:`abc.Messageable` :type channel: :class:`abc.Messageable`
:param user: The user that started typing. :param user: The user that started typing.
:type user: Union[:class:`User`, :class:`Member`] :type user: Union[:class:`User`, :class:`Member`]
:param when: When the typing started as a naive datetime in UTC. :param when: When the typing started as an aware datetime in UTC.
:type when: :class:`datetime.datetime` :type when: :class:`datetime.datetime`
.. function:: on_message(message) .. function:: on_message(message)
@ -612,7 +612,7 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param channel: The private channel that had its pins updated. :param channel: The private channel that had its pins updated.
:type channel: :class:`abc.PrivateChannel` :type channel: :class:`abc.PrivateChannel`
:param last_pin: The latest message that was pinned as a naive datetime in UTC. Could be ``None``. :param last_pin: The latest message that was pinned as an aware datetime in UTC. Could be ``None``.
:type last_pin: Optional[:class:`datetime.datetime`] :type last_pin: Optional[:class:`datetime.datetime`]
.. function:: on_guild_channel_delete(channel) .. function:: on_guild_channel_delete(channel)
@ -646,7 +646,7 @@ to handle it, which defaults to print a traceback and ignoring the exception.
:param channel: The guild channel that had its pins updated. :param channel: The guild channel that had its pins updated.
:type channel: :class:`abc.GuildChannel` :type channel: :class:`abc.GuildChannel`
:param last_pin: The latest message that was pinned as a naive datetime in UTC. Could be ``None``. :param last_pin: The latest message that was pinned as an aware datetime in UTC. Could be ``None``.
:type last_pin: Optional[:class:`datetime.datetime`] :type last_pin: Optional[:class:`datetime.datetime`]
.. function:: on_guild_integrations_update(guild) .. function:: on_guild_integrations_update(guild)

Loading…
Cancel
Save