Skip to content

Commit

Permalink
Add EmbedField object to allow for easier embed class instance crea…
Browse files Browse the repository at this point in the history
…tion (Pycord-Development#1181)

* add EmbedField object to allow for easier embed class instance creation

* add TypeError if `fields` setter is passed a list containing anything other than `EmbedField` objects

* initial pass at removing proxy object reliance from embed fields

* add self._fields to Embed.__init__ (prevents AttributeError)
fix typing for fields parameter in Embed.__init__
fix typing for Embed.fields property
change Embed.add_field, Embed.insert_field_at to use an EmbedField object when appending to Embed._fields
change Embed.set_field_at to set EmbedField properties instead of dictionary values

* add EmbedField to __all__

* fix init loop

* fix init loop

* fix to_dict for _fields attr

* remove now-unused _EmbedFieldProxy class

* add from_dict classmethod to EmbedField for better handling with Embed.from_dict

* update EmbedField.from_dict to more closely match the behavior of Embed.from_dict

* add Embed.append_field option to allow directly adding EmbedField objects without breaking Embed.add_field (similar to Select.append_option)

* add EmbedField to docs

* doc fix

* add docstring for EmbedField.to_dict
  • Loading branch information
krittick authored Apr 17, 2022
1 parent d575fe6 commit 427cbd4
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 45 deletions.
172 changes: 127 additions & 45 deletions discord/embeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
Final,
List,
Mapping,
Optional,
Protocol,
Type,
TypeVar,
Expand All @@ -42,7 +43,10 @@
from . import utils
from .colour import Colour

__all__ = ("Embed",)
__all__ = (
"Embed",
"EmbedField",
)


class _EmptyEmbed:
Expand Down Expand Up @@ -87,11 +91,6 @@ 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]
Expand All @@ -114,6 +113,59 @@ class _EmbedAuthorProxy(Protocol):
proxy_icon_url: MaybeEmpty[str]


class EmbedField:
"""Represents a field on the :class:`Embed` object.
.. versionadded:: 2.0
Attributes
----------
name: :class:`str`
The name of the field.
value: :class:`str`
The value of the field.
inline: :class:`bool`
Whether the field should be displayed inline.
"""

def __init__(self, name: str, value: str, inline: Optional[bool] = False):
self.name = name
self.value = value
self.inline = inline

@classmethod
def from_dict(cls: Type[E], data: Mapping[str, Any]) -> E:
"""Converts a :class:`dict` to a :class:`EmbedField` provided it is in the
format that Discord expects it to be in.
You can find out about this format in the `official Discord documentation`__.
.. _DiscordDocsEF: https://discord.com/developers/docs/resources/channel#embed-object-embed-field-structure
__ DiscordDocsEF_
Parameters
-----------
data: :class:`dict`
The dictionary to convert into an EmbedField object.
"""
self: E = cls.__new__(cls)

self.name = data["name"]
self.value = data["value"]
self.inline = data.get("inline", False)

return self

def to_dict(self) -> Dict[str, Union[str, bool]]:
"""Converts this EmbedField object into a dict."""
return {
"name": self.name,
"value": self.value,
"inline": self.inline,
}


class Embed:
"""Represents a Discord embed.
Expand All @@ -137,7 +189,7 @@ class Embed:
:attr:`Embed.Empty`.
For ease of use, all parameters that expect a :class:`str` are implicitly
casted to :class:`str` for you.
cast to :class:`str` for you.
Attributes
-----------
Expand Down Expand Up @@ -195,6 +247,7 @@ def __init__(
url: MaybeEmpty[Any] = EmptyEmbed,
description: MaybeEmpty[Any] = EmptyEmbed,
timestamp: datetime.datetime = None,
fields: Optional[List[EmbedField]] = None,
):

self.colour = colour if colour is not EmptyEmbed else color
Expand All @@ -214,6 +267,7 @@ def __init__(

if timestamp:
self.timestamp = timestamp
self._fields: List[EmbedField] = fields or []

@classmethod
def from_dict(cls: Type[E], data: Mapping[str, Any]) -> E:
Expand Down Expand Up @@ -271,12 +325,16 @@ def from_dict(cls: Type[E], data: Mapping[str, Any]) -> E:
"image",
"footer",
):
try:
value = data[attr]
except KeyError:
continue
if attr == "fields":
value = data.get(attr, [])
self._fields = [EmbedField.from_dict(d) for d in value] if value else []
else:
setattr(self, f"_{attr}", value)
try:
value = data[attr]
except KeyError:
continue
else:
setattr(self, f"_{attr}", value)

return self

Expand All @@ -287,7 +345,7 @@ def copy(self: E) -> E:
def __len__(self) -> int:
total = len(self.title) + len(self.description)
for field in getattr(self, "_fields", []):
total += len(field["name"]) + len(field["value"])
total += len(field.name) + len(field.value)

try:
footer_text = self._footer["text"]
Expand Down Expand Up @@ -606,16 +664,50 @@ def remove_author(self: E) -> E:
return self

@property
def fields(self) -> List[_EmbedFieldProxy]:
"""List[Union[``EmbedProxy``, :attr:`Empty`]]: Returns a :class:`list` of ``EmbedProxy`` denoting the field contents.
def fields(self) -> Optional[List[EmbedField]]:
"""Returns a :class:`list` of :class:`EmbedField` objects 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.
"""
if self._fields:
return self._fields
else:
return None

@fields.setter
def fields(self, value: List[EmbedField]) -> None:
"""Sets the fields for the embed. This overwrites any existing fields.
Parameters
----------
value: List[:class:`EmbedField`]
The list of :class:`EmbedField` objects to include in the embed.
"""
if not all(isinstance(x, EmbedField) for x in value):
raise TypeError("Expected a list of EmbedField objects.")

self.clear_fields()
for field in value:
self.add_field(name=field.name, value=field.value, inline=field.inline)

def append_field(self, field: EmbedField) -> None:
"""Appends an :class:`EmbedField` object to the embed.
.. versionadded:: 2.0
Parameters
----------
field: :class:`EmbedField`
The field to add.
"""
return [EmbedProxy(d) for d in getattr(self, "_fields", [])] # type: ignore
if not isinstance(field, EmbedField):
raise TypeError("Expected an EmbedField object.")

self._fields.append(field)

def add_field(self: E, *, name: Any, value: Any, inline: bool = True) -> E:
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
Expand All @@ -630,17 +722,7 @@ def add_field(self: E, *, name: Any, value: Any, inline: bool = True) -> E:
inline: :class:`bool`
Whether the field should be displayed inline.
"""

field = {
"inline": inline,
"name": str(name),
"value": str(value),
}

try:
self._fields.append(field)
except AttributeError:
self._fields = [field]
self._fields.append(EmbedField(name=str(name), value=str(value), inline=inline))

return self

Expand All @@ -664,16 +746,9 @@ def insert_field_at(self: E, index: int, *, name: Any, value: Any, inline: bool
Whether the field should be displayed inline.
"""

field = {
"inline": inline,
"name": str(name),
"value": str(value),
}
field = EmbedField(name=str(name), value=str(value), inline=inline)

try:
self._fields.insert(index, field)
except AttributeError:
self._fields = [field]
self._fields.insert(index, field)

return self

Expand Down Expand Up @@ -702,7 +777,7 @@ def remove_field(self, index: int) -> None:
"""
try:
del self._fields[index]
except (AttributeError, IndexError):
except IndexError:
pass

def set_field_at(self: E, index: int, *, name: Any, value: Any, inline: bool = True) -> E:
Expand Down Expand Up @@ -732,19 +807,26 @@ def set_field_at(self: E, index: int, *, name: Any, value: Any, inline: bool = T

try:
field = self._fields[index]
except (TypeError, IndexError, AttributeError):
except (TypeError, IndexError):
raise IndexError("field index out of range")

field["name"] = str(name)
field["value"] = str(value)
field["inline"] = inline
field.name = str(name)
field.value = str(value)
field.inline = inline
return self

def to_dict(self) -> EmbedData:
"""Converts this embed object into a dict."""

# add in the raw data into the dict
result = {key[1:]: getattr(self, key) for key in self.__slots__ if key[0] == "_" and hasattr(self, key)}
result = {
key[1:]: getattr(self, key)
for key in self.__slots__
if key != "_fields" and key[0] == "_" and hasattr(self, key)
}

# add in the fields
result["fields"] = [field.to_dict() for field in self._fields]

# deal with basic convenience wrappers

Expand All @@ -767,7 +849,7 @@ def to_dict(self) -> EmbedData:
else:
result["timestamp"] = timestamp.replace(tzinfo=datetime.timezone.utc).isoformat()

# add in the non raw attribute ones
# add in the non-raw attribute ones
if self.type:
result["type"] = self.type

Expand Down
8 changes: 8 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4673,6 +4673,14 @@ Embed
.. autoclass:: Embed
:members:

EmbedField
~~~~~~~~~~

.. attributetable:: EmbedField

.. autoclass:: EmbedField
:members:

AllowedMentions
~~~~~~~~~~~~~~~~~

Expand Down

0 comments on commit 427cbd4

Please sign in to comment.