From 1bf7aadf943844ed5970a9d44b73d1d67b790b08 Mon Sep 17 00:00:00 2001 From: Rapptz Date: Wed, 5 May 2021 23:48:36 -0400 Subject: [PATCH] Typehint emoji classes --- discord/emoji.py | 84 +++++++++++++++++++++++++--------------- discord/http.py | 6 +-- discord/partial_emoji.py | 53 ++++++++++++++++--------- discord/types/emoji.py | 2 +- 4 files changed, 89 insertions(+), 56 deletions(-) diff --git a/discord/emoji.py b/discord/emoji.py index adc646960..fa246d7fd 100644 --- a/discord/emoji.py +++ b/discord/emoji.py @@ -22,10 +22,11 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import io +from __future__ import annotations +from typing import Any, Iterator, List, Optional, TYPE_CHECKING, Tuple from .asset import Asset, AssetMixin -from . import utils +from .utils import SnowflakeList, snowflake_time, MISSING from .partial_emoji import _EmojiTag from .user import User @@ -33,6 +34,15 @@ __all__ = ( 'Emoji', ) +if TYPE_CHECKING: + from .types.emoji import Emoji as EmojiPayload + from .guild import Guild + from .state import ConnectionState + from .abc import Snowflake + from .role import Role + from datetime import datetime + + class Emoji(_EmojiTag, AssetMixin): """Represents a custom emoji. @@ -82,56 +92,64 @@ class Emoji(_EmojiTag, AssetMixin): The user that created the emoji. This can only be retrieved using :meth:`Guild.fetch_emoji` and having the :attr:`~Permissions.manage_emojis` permission. """ - __slots__ = ('require_colons', 'animated', 'managed', 'id', 'name', '_roles', 'guild_id', - '_state', 'user', 'available') - def __init__(self, *, guild, state, data): + __slots__: Tuple[str, ...] = ( + 'require_colons', + 'animated', + 'managed', + 'id', + 'name', + '_roles', + 'guild_id', + '_state', + 'user', + 'available', + ) + + def __init__(self, *, guild: Guild, state: ConnectionState, data: EmojiPayload): self.guild_id = guild.id self._state = state self._from_data(data) - def _from_data(self, emoji): - self.require_colons = emoji['require_colons'] - self.managed = emoji['managed'] - self.id = int(emoji['id']) + def _from_data(self, emoji: EmojiPayload): + self.require_colons = emoji.get('require_colons', False) + self.managed = emoji.get('managed', False) + self.id = int(emoji['id']) # type: ignore self.name = emoji['name'] self.animated = emoji.get('animated', False) self.available = emoji.get('available', True) - self._roles = utils.SnowflakeList(map(int, emoji.get('roles', []))) + self._roles = SnowflakeList(map(int, emoji.get('roles', []))) user = emoji.get('user') self.user = User(state=self._state, data=user) if user else None - def _iterator(self): + def __iter__(self) -> Iterator[Tuple[str, Any]]: for attr in self.__slots__: if attr[0] != '_': value = getattr(self, attr, None) if value is not None: yield (attr, value) - def __iter__(self): - return self._iterator() - - def __str__(self): + def __str__(self) -> str: if self.animated: return f'' return f'<:{self.name}:{self.id}>' - def __repr__(self): + def __repr__(self) -> str: return f'' - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: return isinstance(other, _EmojiTag) and self.id == other.id - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: return not self.__eq__(other) - def __hash__(self): + def __hash__(self) -> int: return self.id >> 22 @property - def created_at(self): + def created_at(self) -> datetime: """:class:`datetime.datetime`: Returns the emoji's creation time in UTC.""" - return utils.snowflake_time(self.id) + return snowflake_time(self.id) @property def url(self) -> str: @@ -140,7 +158,7 @@ class Emoji(_EmojiTag, AssetMixin): return f'{Asset.BASE}/emojis/{self.id}.{fmt}' @property - def roles(self): + def roles(self) -> List[Role]: """List[:class:`Role`]: A :class:`list` of roles that is allowed to use this emoji. If roles is empty, the emoji is unrestricted. @@ -152,11 +170,11 @@ class Emoji(_EmojiTag, AssetMixin): return [role for role in guild.roles if self._roles.has(role.id)] @property - def guild(self): + def guild(self) -> Guild: """:class:`Guild`: The guild this emoji belongs to.""" return self._state._get_guild(self.guild_id) - def is_usable(self): + def is_usable(self) -> bool: """:class:`bool`: Whether the bot can use this emoji. .. versionadded:: 1.3 @@ -168,7 +186,7 @@ class Emoji(_EmojiTag, AssetMixin): emoji_roles, my_roles = self._roles, self.guild.me._roles return any(my_roles.has(role_id) for role_id in emoji_roles) - async def delete(self, *, reason=None): + async def delete(self, *, reason: Optional[str] = None) -> None: """|coro| Deletes the custom emoji. @@ -191,7 +209,7 @@ class Emoji(_EmojiTag, AssetMixin): await self._state.http.delete_custom_emoji(self.guild.id, self.id, reason=reason) - async def edit(self, *, name=None, roles=None, reason=None): + async def edit(self, *, name: str = MISSING, roles: List[Snowflake] = MISSING, reason: Optional[str] = None) -> None: r"""|coro| Edits the custom emoji. @@ -203,8 +221,8 @@ class Emoji(_EmojiTag, AssetMixin): ----------- name: :class:`str` The new emoji name. - roles: Optional[list[:class:`Role`]] - A :class:`list` of :class:`Role`\s that can use this emoji. Leave empty to make it available to everyone. + roles: Optional[List[:class:`~discord.abc.Snowflake`]] + A list of roles that can use this emoji. An empty list can be passed to make it available to everyone. reason: Optional[:class:`str`] The reason for editing this emoji. Shows up on the audit log. @@ -216,8 +234,10 @@ class Emoji(_EmojiTag, AssetMixin): An error occurred editing the emoji. """ - name = name or self.name - if roles: - roles = [role.id for role in roles] - await self._state.http.edit_custom_emoji(self.guild.id, self.id, name=name, roles=roles, reason=reason) + payload = {} + if name is not MISSING: + payload['name'] = name + if roles is not MISSING: + payload['roles'] = [role.id for role in roles] + await self._state.http.edit_custom_emoji(self.guild.id, self.id, payload=payload, reason=reason) diff --git a/discord/http.py b/discord/http.py index 471d829e6..1b93cc351 100644 --- a/discord/http.py +++ b/discord/http.py @@ -905,11 +905,7 @@ class HTTPClient: r = Route('DELETE', '/guilds/{guild_id}/emojis/{emoji_id}', guild_id=guild_id, emoji_id=emoji_id) return self.request(r, reason=reason) - def edit_custom_emoji(self, guild_id, emoji_id, *, name, roles=None, reason=None): - payload = { - 'name': name, - 'roles': roles or [], - } + def edit_custom_emoji(self, guild_id: int, emoji_id: int, *, payload, reason: Optional[str] = None): r = Route('PATCH', '/guilds/{guild_id}/emojis/{emoji_id}', guild_id=guild_id, emoji_id=emoji_id) return self.request(r, json=payload, reason=reason) diff --git a/discord/partial_emoji.py b/discord/partial_emoji.py index 82203bf53..83fc91921 100644 --- a/discord/partial_emoji.py +++ b/discord/partial_emoji.py @@ -22,19 +22,31 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ -import io +from __future__ import annotations +from typing import Any, Dict, Optional, TYPE_CHECKING, Type, TypeVar from .asset import Asset, AssetMixin -from .errors import DiscordException, InvalidArgument +from .errors import InvalidArgument from . import utils __all__ = ( 'PartialEmoji', ) +if TYPE_CHECKING: + from .state import ConnectionState + from datetime import datetime + + class _EmojiTag: __slots__ = () + id: int + + +PE = TypeVar('PE', bound='PartialEmoji') + + class PartialEmoji(_EmojiTag, AssetMixin): """Represents a "partial" emoji. @@ -75,22 +87,25 @@ class PartialEmoji(_EmojiTag, AssetMixin): __slots__ = ('animated', 'name', 'id', '_state') - def __init__(self, *, name, animated=False, id=None): + if TYPE_CHECKING: + id: Optional[int] + + def __init__(self, *, name: str, animated: bool = False, id: Optional[int] = None): self.animated = animated self.name = name self.id = id - self._state = None + self._state: Optional[ConnectionState] = None @classmethod - def from_dict(cls, data): + def from_dict(cls: Type[PE], data: Dict[str, Any]) -> PE: return cls( animated=data.get('animated', False), id=utils._get_as_snowflake(data, 'id'), - name=data.get('name'), + name=data.get('name', ''), ) - def to_dict(self): - o = { 'name': self.name } + def to_dict(self) -> Dict[str, Any]: + o: Dict[str, Any] = {'name': self.name} if self.id: o['id'] = self.id if self.animated: @@ -98,12 +113,14 @@ class PartialEmoji(_EmojiTag, AssetMixin): return o @classmethod - def with_state(cls, state, *, name, animated=False, id=None): + def with_state( + cls: Type[PE], state: ConnectionState, *, name: str, animated: bool = False, id: Optional[int] = None + ) -> PE: self = cls(name=name, animated=animated, id=id) self._state = state return self - def __str__(self): + def __str__(self) -> str: if self.id is None: return self.name if self.animated: @@ -113,7 +130,7 @@ class PartialEmoji(_EmojiTag, AssetMixin): def __repr__(self): return f'<{self.__class__.__name__} animated={self.animated} name={self.name!r} id={self.id}>' - def __eq__(self, other): + def __eq__(self, other: Any) -> bool: if self.is_unicode_emoji(): return isinstance(other, PartialEmoji) and self.name == other.name @@ -121,32 +138,32 @@ class PartialEmoji(_EmojiTag, AssetMixin): return self.id == other.id return False - def __ne__(self, other): + def __ne__(self, other: Any) -> bool: return not self.__eq__(other) - def __hash__(self): + def __hash__(self) -> int: return hash((self.id, self.name)) - def is_custom_emoji(self): + def is_custom_emoji(self) -> bool: """:class:`bool`: Checks if this is a custom non-Unicode emoji.""" return self.id is not None - def is_unicode_emoji(self): + def is_unicode_emoji(self) -> bool: """:class:`bool`: Checks if this is a Unicode emoji.""" return self.id is None - def _as_reaction(self): + def _as_reaction(self) -> str: if self.id is None: return self.name return f'{self.name}:{self.id}' @property - def created_at(self): + def created_at(self) -> Optional[datetime]: """Optional[:class:`datetime.datetime`]: Returns the emoji's creation time in UTC, or None if Unicode emoji. .. versionadded:: 1.6 """ - if self.is_unicode_emoji(): + if self.id is None: return None return utils.snowflake_time(self.id) diff --git a/discord/types/emoji.py b/discord/types/emoji.py index 7c5e4d80e..94121bf9b 100644 --- a/discord/types/emoji.py +++ b/discord/types/emoji.py @@ -35,7 +35,7 @@ class PartialEmoji(TypedDict): class Emoji(PartialEmoji, total=False): roles: SnowflakeList user: User - required_colons: bool + require_colons: bool managed: bool animated: bool available: bool