Typehint discord.Embed and introduce discord.types subpackage

The discord.types subpackage is currently only used to do type hinting
of API payloads, it's very much incomplete right now and it'll be a
rather long process.

discord.Embed was typehinted and formatted using black.
This commit is contained in:
Rapptz
2021-04-07 05:38:01 -04:00
parent bda3afcad7
commit 23fe6b46dd
4 changed files with 249 additions and 90 deletions

View File

@ -22,7 +22,10 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
from __future__ import annotations
import datetime
from typing import Any, Dict, Final, List, Protocol, TYPE_CHECKING, Type, TypeVar, Union
from . import utils
from .colour import Colour
@ -31,32 +34,75 @@ __all__ = (
'Embed',
)
class _EmptyEmbed:
def __bool__(self):
def __bool__(self) -> bool:
return False
def __repr__(self):
def __repr__(self) -> str:
return 'Embed.Empty'
def __len__(self):
def __len__(self) -> int:
return 0
EmptyEmbed = _EmptyEmbed()
EmptyEmbed: Final = _EmptyEmbed()
class EmbedProxy:
def __init__(self, layer):
def __init__(self, layer: Dict[str, Any]):
self.__dict__.update(layer)
def __len__(self):
def __len__(self) -> int:
return len(self.__dict__)
def __repr__(self):
def __repr__(self) -> str:
inner = ', '.join((f'{k}={v!r}' for k, v in self.__dict__.items() if not k.startswith('_')))
return f'EmbedProxy({inner})'
def __getattr__(self, attr):
def __getattr__(self, attr: str) -> _EmptyEmbed:
return EmptyEmbed
E = TypeVar('E', bound='Embed')
if TYPE_CHECKING:
from discord.types.common import Embed as EmbedData, EmbedType
T = TypeVar('T')
MaybeEmpty = Union[T, _EmptyEmbed]
class _EmbedFooterProxy(Protocol):
text: MaybeEmpty[str]
icon_url: MaybeEmpty[str]
class _EmbedFieldProxy(Protocol):
name: MaybeEmpty[str]
value: MaybeEmpty[str]
inline: bool
class _EmbedMediaProxy(Protocol):
url: MaybeEmpty[str]
proxy_url: MaybeEmpty[str]
height: MaybeEmpty[int]
width: MaybeEmpty[int]
class _EmbedVideoProxy(Protocol):
url: MaybeEmpty[str]
height: MaybeEmpty[int]
width: MaybeEmpty[int]
class _EmbedProviderProxy(Protocol):
name: MaybeEmpty[str]
url: MaybeEmpty[str]
class _EmbedAuthorProxy(Protocol):
name: MaybeEmpty[str]
url: MaybeEmpty[str]
icon_url: MaybeEmpty[str]
proxy_icon_url: MaybeEmpty[str]
class Embed:
"""Represents a Discord embed.
@ -108,24 +154,41 @@ class Embed:
to denote that the value or attribute is empty.
"""
__slots__ = ('title', 'url', 'type', '_timestamp', '_colour', '_footer',
'_image', '_thumbnail', '_video', '_provider', '_author',
'_fields', 'description')
__slots__ = (
'title',
'url',
'type',
'_timestamp',
'_colour',
'_footer',
'_image',
'_thumbnail',
'_video',
'_provider',
'_author',
'_fields',
'description',
)
Empty = EmptyEmbed
Empty: Final = EmptyEmbed
def __init__(self, **kwargs):
# swap the colour/color aliases
try:
colour = kwargs['colour']
except KeyError:
colour = kwargs.get('color', EmptyEmbed)
def __init__(
self,
*,
colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed,
title: MaybeEmpty[str] = EmptyEmbed,
type: EmbedType = 'rich',
url: MaybeEmpty[str] = EmptyEmbed,
description: MaybeEmpty[str] = EmptyEmbed,
timestamp: datetime.datetime = None,
):
self.colour = colour
self.title = kwargs.get('title', EmptyEmbed)
self.type = kwargs.get('type', 'rich')
self.url = kwargs.get('url', EmptyEmbed)
self.description = kwargs.get('description', EmptyEmbed)
self.colour = colour if colour is not EmptyEmbed else color
self.title = title
self.type = type
self.url = url
self.description = description
if self.title is not EmptyEmbed:
self.title = str(self.title)
@ -136,17 +199,13 @@ class Embed:
if self.url is not EmptyEmbed:
self.url = str(self.url)
try:
timestamp = kwargs['timestamp']
except KeyError:
pass
else:
if timestamp:
if timestamp.tzinfo is None:
timestamp = timestamp.astimezone()
self.timestamp = timestamp
@classmethod
def from_dict(cls, data):
def from_dict(cls: Type[E], data: EmbedData) -> E:
"""Converts a :class:`dict` to a :class:`Embed` provided it is in the
format that Discord expects it to be in.
@ -162,7 +221,7 @@ class Embed:
The dictionary to convert into an embed.
"""
# we are bypassing __init__ here since it doesn't apply here
self = cls.__new__(cls)
self: E = cls.__new__(cls)
# fill in the basic fields
@ -202,11 +261,11 @@ class Embed:
return self
def copy(self):
def copy(self: E) -> E:
"""Returns a shallow copy of the embed."""
return Embed.from_dict(self.to_dict())
return self.__class__.from_dict(self.to_dict())
def __len__(self):
def __len__(self) -> int:
total = len(self.title) + len(self.description)
for field in getattr(self, '_fields', []):
total += len(field['name']) + len(field['value'])
@ -227,28 +286,30 @@ class Embed:
return total
def __bool__(self):
return any((
self.title,
self.url,
self.description,
self.colour,
self.fields,
self.timestamp,
self.author,
self.thumbnail,
self.footer,
self.image,
self.provider,
self.video,
))
def __bool__(self) -> bool:
return any(
(
self.title,
self.url,
self.description,
self.colour,
self.fields,
self.timestamp,
self.author,
self.thumbnail,
self.footer,
self.image,
self.provider,
self.video,
)
)
@property
def colour(self):
def colour(self) -> MaybeEmpty[Colour]:
return getattr(self, '_colour', EmptyEmbed)
@colour.setter
def colour(self, value):
def colour(self, value: Union[int, Colour, _EmptyEmbed]): # type: ignore
if isinstance(value, (Colour, _EmptyEmbed)):
self._colour = value
elif isinstance(value, int):
@ -259,27 +320,27 @@ class Embed:
color = colour
@property
def timestamp(self):
def timestamp(self) -> MaybeEmpty[datetime.datetime]:
return getattr(self, '_timestamp', EmptyEmbed)
@timestamp.setter
def timestamp(self, value):
def timestamp(self, value: MaybeEmpty[datetime.datetime]):
if isinstance(value, (datetime.datetime, _EmptyEmbed)):
self._timestamp = value
else:
raise TypeError(f"Expected datetime.datetime or Embed.Empty received {value.__class__.__name__} instead")
@property
def footer(self):
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the footer contents.
def footer(self) -> _EmbedFooterProxy:
"""Returns an ``EmbedProxy`` denoting the footer contents.
See :meth:`set_footer` for possible values you can access.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_footer', {}))
return EmbedProxy(getattr(self, '_footer', {})) # type: ignore
def set_footer(self, *, text=EmptyEmbed, icon_url=EmptyEmbed):
def set_footer(self: E, *, text: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
"""Sets the footer for the embed content.
This function returns the class instance to allow for fluent-style
@ -303,8 +364,8 @@ class Embed:
return self
@property
def image(self):
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the image contents.
def image(self) -> _EmbedMediaProxy:
"""Returns an ``EmbedProxy`` denoting the image contents.
Possible attributes you can access are:
@ -315,9 +376,9 @@ class Embed:
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_image', {}))
return EmbedProxy(getattr(self, '_image', {})) # type: ignore
def set_image(self, *, url):
def set_image(self: E, *, url: MaybeEmpty[str]) -> E:
"""Sets the image for the embed content.
This function returns the class instance to allow for fluent-style
@ -339,14 +400,14 @@ class Embed:
pass
else:
self._image = {
'url': str(url)
'url': str(url),
}
return self
@property
def thumbnail(self):
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the thumbnail contents.
def thumbnail(self) -> _EmbedMediaProxy:
"""Returns an ``EmbedProxy`` denoting the thumbnail contents.
Possible attributes you can access are:
@ -357,9 +418,9 @@ class Embed:
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_thumbnail', {}))
return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore
def set_thumbnail(self, *, url):
def set_thumbnail(self: E, *, url: MaybeEmpty[str]) -> E:
"""Sets the thumbnail for the embed content.
This function returns the class instance to allow for fluent-style
@ -381,14 +442,14 @@ class Embed:
pass
else:
self._thumbnail = {
'url': str(url)
'url': str(url),
}
return self
@property
def video(self):
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the video contents.
def video(self) -> _EmbedVideoProxy:
"""Returns an ``EmbedProxy`` denoting the video contents.
Possible attributes include:
@ -398,29 +459,29 @@ class Embed:
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_video', {}))
return EmbedProxy(getattr(self, '_video', {})) # type: ignore
@property
def provider(self):
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the provider contents.
def provider(self) -> _EmbedProviderProxy:
"""Returns an ``EmbedProxy`` denoting the provider contents.
The only attributes that might be accessed are ``name`` and ``url``.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_provider', {}))
return EmbedProxy(getattr(self, '_provider', {})) # type: ignore
@property
def author(self):
"""Union[:class:`EmbedProxy`, :attr:`Empty`]: Returns an ``EmbedProxy`` denoting the author contents.
def author(self) -> _EmbedAuthorProxy:
"""Returns an ``EmbedProxy`` denoting the author contents.
See :meth:`set_author` for possible values you can access.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_author', {}))
return EmbedProxy(getattr(self, '_author', {})) # type: ignore
def set_author(self, *, name, url=EmptyEmbed, icon_url=EmptyEmbed):
def set_author(self: E, *, name: str, url: MaybeEmpty[str] = EmptyEmbed, icon_url: MaybeEmpty[str] = EmptyEmbed) -> E:
"""Sets the author for the embed content.
This function returns the class instance to allow for fluent-style
@ -437,7 +498,7 @@ class Embed:
"""
self._author = {
'name': str(name)
'name': str(name),
}
if url is not EmptyEmbed:
@ -448,7 +509,7 @@ class Embed:
return self
def remove_author(self):
def remove_author(self: E) -> E:
"""Clears embed's author information.
This function returns the class instance to allow for fluent-style
@ -464,16 +525,16 @@ class Embed:
return self
@property
def fields(self):
def fields(self) -> List[_EmbedFieldProxy]:
"""Union[List[:class:`EmbedProxy`], :attr:`Empty`]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
See :meth:`add_field` for possible values you can access.
If the attribute has no value then :attr:`Empty` is returned.
"""
return [EmbedProxy(d) for d in getattr(self, '_fields', [])]
return [EmbedProxy(d) for d in getattr(self, '_fields', [])] # type: ignore
def add_field(self, *, name, value, inline=True):
def add_field(self: E, *, name: str, value: str, inline: bool = True) -> E:
"""Adds a field to the embed object.
This function returns the class instance to allow for fluent-style
@ -492,7 +553,7 @@ class Embed:
field = {
'inline': inline,
'name': str(name),
'value': str(value)
'value': str(value),
}
try:
@ -502,7 +563,7 @@ class Embed:
return self
def insert_field_at(self, index, *, name, value, inline=True):
def insert_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
"""Inserts a field before a specified index to the embed.
This function returns the class instance to allow for fluent-style
@ -525,7 +586,7 @@ class Embed:
field = {
'inline': inline,
'name': str(name),
'value': str(value)
'value': str(value),
}
try:
@ -535,14 +596,14 @@ class Embed:
return self
def clear_fields(self):
def clear_fields(self) -> None:
"""Removes all fields from this embed."""
try:
self._fields.clear()
except AttributeError:
self._fields = []
def remove_field(self, index):
def remove_field(self, index: int) -> None:
"""Removes a field at a specified index.
If the index is invalid or out of bounds then the error is
@ -563,7 +624,7 @@ class Embed:
except (AttributeError, IndexError):
pass
def set_field_at(self, index, *, name, value, inline=True):
def set_field_at(self: E, index: int, *, name: str, value: str, inline: bool = True) -> E:
"""Modifies a field to the embed object.
The index must point to a valid pre-existing field.
@ -598,15 +659,17 @@ class Embed:
field['inline'] = inline
return self
def to_dict(self):
def to_dict(self) -> EmbedData:
"""Converts this embed object into a dict."""
# add in the raw data into the dict
# fmt: off
result = {
key[1:]: getattr(self, key)
for key in self.__slots__
if key[0] == '_' and hasattr(self, key)
}
# fmt: on
# deal with basic convenience wrappers
@ -642,4 +705,4 @@ class Embed:
if self.title:
result['title'] = self.title
return result
return result # type: ignore