diff --git a/discord/embeds.py b/discord/embeds.py index cb710d6e4..8ea4086a8 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -25,7 +25,7 @@ DEALINGS IN THE SOFTWARE. from __future__ import annotations import datetime -from typing import Any, Dict, Final, List, Mapping, Protocol, TYPE_CHECKING, TypeVar, Union +from typing import Any, Dict, List, Mapping, Optional, Protocol, TYPE_CHECKING, TypeVar, Union from . import utils from .colour import Colour @@ -37,20 +37,6 @@ __all__ = ( # fmt: on -class _EmptyEmbed: - def __bool__(self) -> bool: - return False - - def __repr__(self) -> str: - return 'Embed.Empty' - - def __len__(self) -> int: - return 0 - - -EmptyEmbed: Final = _EmptyEmbed() - - class EmbedProxy: def __init__(self, layer: Dict[str, Any]): self.__dict__.update(layer) @@ -62,8 +48,8 @@ class EmbedProxy: 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: str) -> _EmptyEmbed: - return EmptyEmbed + def __getattr__(self, attr: str) -> None: + return None if TYPE_CHECKING: @@ -72,37 +58,36 @@ if TYPE_CHECKING: from .types.embed import Embed as EmbedData, EmbedType T = TypeVar('T') - MaybeEmpty = Union[T, _EmptyEmbed] class _EmbedFooterProxy(Protocol): - text: MaybeEmpty[str] - icon_url: MaybeEmpty[str] + text: Optional[str] + icon_url: Optional[str] class _EmbedFieldProxy(Protocol): - name: MaybeEmpty[str] - value: MaybeEmpty[str] + name: Optional[str] + value: Optional[str] inline: bool class _EmbedMediaProxy(Protocol): - url: MaybeEmpty[str] - proxy_url: MaybeEmpty[str] - height: MaybeEmpty[int] - width: MaybeEmpty[int] + url: Optional[str] + proxy_url: Optional[str] + height: Optional[int] + width: Optional[int] class _EmbedVideoProxy(Protocol): - url: MaybeEmpty[str] - height: MaybeEmpty[int] - width: MaybeEmpty[int] + url: Optional[str] + height: Optional[int] + width: Optional[int] class _EmbedProviderProxy(Protocol): - name: MaybeEmpty[str] - url: MaybeEmpty[str] + name: Optional[str] + url: Optional[str] class _EmbedAuthorProxy(Protocol): - name: MaybeEmpty[str] - url: MaybeEmpty[str] - icon_url: MaybeEmpty[str] - proxy_icon_url: MaybeEmpty[str] + name: Optional[str] + url: Optional[str] + icon_url: Optional[str] + proxy_icon_url: Optional[str] class Embed: @@ -121,18 +106,15 @@ class Embed: .. versionadded:: 2.0 - Certain properties return an ``EmbedProxy``, a type - that acts similar to a regular :class:`dict` except using dotted access, - e.g. ``embed.author.icon_url``. If the attribute - is invalid or empty, then a special sentinel value is returned, - :attr:`Embed.Empty`. - For ease of use, all parameters that expect a :class:`str` are implicitly casted to :class:`str` for you. + .. versionchanged:: 2.0 + ``Embed.Empty`` has been removed in favour of ``None``. + Attributes ----------- - title: :class:`str` + title: Optional[:class:`str`] The title of the embed. This can be set during initialisation. type: :class:`str` @@ -140,22 +122,19 @@ class Embed: This can be set during initialisation. Possible strings for embed types can be found on discord's `api docs `_ - description: :class:`str` + description: Optional[:class:`str`] The description of the embed. This can be set during initialisation. - url: :class:`str` + url: Optional[:class:`str`] The URL of the embed. This can be set during initialisation. - timestamp: :class:`datetime.datetime` + timestamp: Optional[:class:`datetime.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: Optional[Union[:class:`Colour`, :class:`int`]] The colour code of the embed. Aliased to ``color`` as well. This can be set during initialisation. - Empty - A special sentinel value used by ``EmbedProxy`` and this class - to denote that the value or attribute is empty. """ __slots__ = ( @@ -174,36 +153,34 @@ class Embed: 'description', ) - Empty: Final = EmptyEmbed - def __init__( self, *, - colour: Union[int, Colour, _EmptyEmbed] = EmptyEmbed, - color: Union[int, Colour, _EmptyEmbed] = EmptyEmbed, - title: MaybeEmpty[Any] = EmptyEmbed, + colour: Optional[Union[int, Colour]] = None, + color: Optional[Union[int, Colour]] = None, + title: Optional[Any] = None, type: EmbedType = 'rich', - url: MaybeEmpty[Any] = EmptyEmbed, - description: MaybeEmpty[Any] = EmptyEmbed, - timestamp: MaybeEmpty[datetime.datetime] = EmptyEmbed, + url: Optional[Any] = None, + description: Optional[Any] = None, + timestamp: Optional[datetime.datetime] = None, ): - self.colour = colour if colour is not EmptyEmbed else color - self.title: MaybeEmpty[str] = title + self.colour = colour if colour is not None else color + self.title: Optional[str] = title self.type: EmbedType = type - self.url: MaybeEmpty[str] = url - self.description: MaybeEmpty[str] = description + self.url: Optional[str] = url + self.description: Optional[str] = description - if self.title is not EmptyEmbed: + if self.title is not None: self.title = str(self.title) - if self.description is not EmptyEmbed: + if self.description is not None: self.description = str(self.description) - if self.url is not EmptyEmbed: + if self.url is not None: self.url = str(self.url) - if timestamp is not EmptyEmbed: + if timestamp is not None: self.timestamp = timestamp @classmethod @@ -227,18 +204,18 @@ class Embed: # fill in the basic fields - self.title = data.get('title', EmptyEmbed) - self.type = data.get('type', EmptyEmbed) - self.description = data.get('description', EmptyEmbed) - self.url = data.get('url', EmptyEmbed) + self.title = data.get('title', None) + self.type = data.get('type', None) + self.description = data.get('description', None) + self.url = data.get('url', None) - if self.title is not EmptyEmbed: + if self.title is not None: self.title = str(self.title) - if self.description is not EmptyEmbed: + if self.description is not None: self.description = str(self.description) - if self.url is not EmptyEmbed: + if self.url is not None: self.url = str(self.url) # try to fill in the more rich fields @@ -268,7 +245,7 @@ class Embed: return self.__class__.from_dict(self.to_dict()) def __len__(self) -> int: - total = len(self.title) + len(self.description) + total = len(self.title or '') + len(self.description or '') for field in getattr(self, '_fields', []): total += len(field['name']) + len(field['value']) @@ -307,34 +284,36 @@ class Embed: ) @property - def colour(self) -> MaybeEmpty[Colour]: - return getattr(self, '_colour', EmptyEmbed) + def colour(self) -> Optional[Colour]: + return getattr(self, '_colour', None) @colour.setter - def colour(self, value: Union[int, Colour, _EmptyEmbed]) -> None: - if isinstance(value, (Colour, _EmptyEmbed)): + def colour(self, value: Optional[Union[int, Colour]]) -> None: + if value is None: + self._colour = None + elif isinstance(value, Colour): self._colour = value elif isinstance(value, int): self._colour = Colour(value=value) else: - raise TypeError(f'Expected discord.Colour, int, or Embed.Empty but received {value.__class__.__name__} instead.') + raise TypeError(f'Expected discord.Colour, int, or None but received {value.__class__.__name__} instead.') color = colour @property - def timestamp(self) -> MaybeEmpty[datetime.datetime]: - return getattr(self, '_timestamp', EmptyEmbed) + def timestamp(self) -> Optional[datetime.datetime]: + return getattr(self, '_timestamp', None) @timestamp.setter - def timestamp(self, value: MaybeEmpty[datetime.datetime]) -> None: + def timestamp(self, value: Optional[datetime.datetime]) -> None: if isinstance(value, datetime.datetime): if value.tzinfo is None: value = value.astimezone() self._timestamp = value - elif isinstance(value, _EmptyEmbed): - self._timestamp = value + elif value is None: + self._timestamp = None else: - raise TypeError(f"Expected datetime.datetime or Embed.Empty received {value.__class__.__name__} instead") + raise TypeError(f"Expected datetime.datetime or None received {value.__class__.__name__} instead") @property def footer(self) -> _EmbedFooterProxy: @@ -342,12 +321,12 @@ class Embed: See :meth:`set_footer` for possible values you can access. - If the attribute has no value then :attr:`Empty` is returned. + If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. return EmbedProxy(getattr(self, '_footer', {})) # type: ignore - def set_footer(self, *, text: MaybeEmpty[Any] = EmptyEmbed, icon_url: MaybeEmpty[Any] = EmptyEmbed) -> Self: + def set_footer(self, *, text: Optional[Any] = None, icon_url: Optional[Any] = None) -> Self: """Sets the footer for the embed content. This function returns the class instance to allow for fluent-style @@ -362,10 +341,10 @@ class Embed: """ self._footer = {} - if text is not EmptyEmbed: + if text is not None: self._footer['text'] = str(text) - if icon_url is not EmptyEmbed: + if icon_url is not None: self._footer['icon_url'] = str(icon_url) return self @@ -396,27 +375,24 @@ class Embed: - ``width`` - ``height`` - If the attribute has no value then :attr:`Empty` is returned. + If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. return EmbedProxy(getattr(self, '_image', {})) # type: ignore - def set_image(self, *, url: MaybeEmpty[Any]) -> Self: + def set_image(self, *, url: Optional[Any]) -> Self: """Sets the image for the embed content. This function returns the class instance to allow for fluent-style chaining. - .. versionchanged:: 1.4 - Passing :attr:`Empty` removes the image. - Parameters ----------- url: :class:`str` The source URL for the image. Only HTTP(S) is supported. """ - if url is EmptyEmbed: + if url is None: try: del self._image except AttributeError: @@ -439,19 +415,19 @@ class Embed: - ``width`` - ``height`` - If the attribute has no value then :attr:`Empty` is returned. + If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. return EmbedProxy(getattr(self, '_thumbnail', {})) # type: ignore - def set_thumbnail(self, *, url: MaybeEmpty[Any]) -> Self: + def set_thumbnail(self, *, url: Optional[Any]) -> Self: """Sets the thumbnail for the embed content. This function returns the class instance to allow for fluent-style chaining. .. versionchanged:: 1.4 - Passing :attr:`Empty` removes the thumbnail. + Passing ``None`` removes the thumbnail. Parameters ----------- @@ -459,7 +435,7 @@ class Embed: The source URL for the thumbnail. Only HTTP(S) is supported. """ - if url is EmptyEmbed: + if url is None: try: del self._thumbnail except AttributeError: @@ -481,7 +457,7 @@ class Embed: - ``height`` for the video height. - ``width`` for the video width. - If the attribute has no value then :attr:`Empty` is returned. + If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. return EmbedProxy(getattr(self, '_video', {})) # type: ignore @@ -492,7 +468,7 @@ class Embed: The only attributes that might be accessed are ``name`` and ``url``. - If the attribute has no value then :attr:`Empty` is returned. + If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. return EmbedProxy(getattr(self, '_provider', {})) # type: ignore @@ -503,12 +479,12 @@ class Embed: See :meth:`set_author` for possible values you can access. - If the attribute has no value then :attr:`Empty` is returned. + If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. return EmbedProxy(getattr(self, '_author', {})) # type: ignore - def set_author(self, *, name: Any, url: MaybeEmpty[Any] = EmptyEmbed, icon_url: MaybeEmpty[Any] = EmptyEmbed) -> Self: + def set_author(self, *, name: Any, url: Optional[Any] = None, icon_url: Optional[Any] = None) -> Self: """Sets the author for the embed content. This function returns the class instance to allow for fluent-style @@ -528,10 +504,10 @@ class Embed: 'name': str(name), } - if url is not EmptyEmbed: + if url is not None: self._author['url'] = str(url) - if icon_url is not EmptyEmbed: + if icon_url is not None: self._author['icon_url'] = str(icon_url) return self @@ -553,11 +529,11 @@ class Embed: @property def fields(self) -> List[_EmbedFieldProxy]: - """List[Union[``EmbedProxy``, :attr:`Empty`]]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents. + """List[``EmbedProxy``]: 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. + If the attribute has no value then ``None`` is returned. """ # Lying to the type checker for better developer UX. return [EmbedProxy(d) for d in getattr(self, '_fields', [])] # type: ignore diff --git a/docs/migrating.rst b/docs/migrating.rst index cbea6d9db..e73101baa 100644 --- a/docs/migrating.rst +++ b/docs/migrating.rst @@ -842,6 +842,25 @@ The return type of the following methods has been changed to an :term:`asynchron The ``NoMoreItems`` exception was removed as calling :func:`anext` or :meth:`~object.__anext__` on an :term:`asynchronous iterator` will now raise :class:`StopAsyncIteration`. +Removal of ``Embed.Empty`` +--------------------------- + +Originally, embeds used a special sentinel to denote emptiness or remove an attribute from display. The ``Embed.Empty`` sentinel was made when Discord's embed design was in a nebulous state of flux. Since then, the embed design has stabilised and thus the sentinel is seen as legacy. + +Therefore, ``Embed.Empty`` has been removed in favour of ``None``. + +.. code-block:: python + + # Before + + embed = discord.Embed(title='foo') + embed.title = discord.Embed.Empty + + # After + embed = discord.Embed(title='foo') + embed.title = None + + Removal of ``InvalidArgument`` Exception -------------------------------------------