Browse Source

Make discord.Embed builder more strict and easier to use.

Allow for easier use when trying to "reuse" the same discord.Embed
object by providing new methods such as Embed.clear_fields,
Embed.set_field_at, and allowing you to set things to Embed.Empty to
clear out an attribute.

For ease of use, things are automatically casted to ``str`` to prevent
the user from having HTTP 400 errors if they forgot to do so. The new
embed builder also supports "fluent-style" interface to allow you to
chain methods in a single line if necessary.

Certain parameters were removed since they were ignored by Discord
anyway such as `width` and `height` in Embed.set_image and
Embed.set_thumbnail.
pull/408/head
Rapptz 8 years ago
parent
commit
c4ee4c1db4
  1. 217
      discord/embeds.py

217
discord/embeds.py

@ -60,6 +60,9 @@ class Embed:
is invalid or empty, then a special sentinel value is returned,
:attr:`Embed.Empty`.
For ease of use, all parameters that expect a ``str`` are implicitly
casted to ``str`` for you.
Attributes
-----------
title: str
@ -75,8 +78,8 @@ class Embed:
colour: :class:`Colour` or int
The colour code of the embed. Aliased to ``color`` as well.
Empty
A special sentinel value used by ``EmbedProxy`` to denote
that the value or attribute is empty.
A special sentinel value used by ``EmbedProxy`` and this class
to denote that the value or attribute is empty.
"""
__slots__ = ('title', 'url', 'type', '_timestamp', '_colour', '_footer',
@ -90,15 +93,13 @@ class Embed:
try:
colour = kwargs['colour']
except KeyError:
colour = kwargs.get('color')
if colour is not None:
self.colour = colour
colour = kwargs.get('color', EmptyEmbed)
self.title = kwargs.get('title')
self.colour = colour
self.title = kwargs.get('title', EmptyEmbed)
self.type = kwargs.get('type', 'rich')
self.url = kwargs.get('url')
self.description = kwargs.get('description')
self.url = kwargs.get('url', EmptyEmbed)
self.description = kwargs.get('description', EmptyEmbed)
try:
timestamp = kwargs['timestamp']
@ -114,10 +115,10 @@ class Embed:
# fill in the basic fields
self.title = data.get('title')
self.type = data.get('type')
self.description = data.get('description')
self.url = data.get('url')
self.title = data.get('title', EmptyEmbed)
self.type = data.get('type', EmptyEmbed)
self.description = data.get('description', EmptyEmbed)
self.url = data.get('url', EmptyEmbed)
# try to fill in the more rich fields
@ -143,29 +144,29 @@ class Embed:
@property
def colour(self):
return getattr(self, '_colour', None)
return getattr(self, '_colour', EmptyEmbed)
@colour.setter
def colour(self, value):
if isinstance(value, Colour):
if isinstance(value, (Colour, _EmptyEmbed)):
self._colour = value
elif isinstance(value, int):
self._colour = Colour(value=value)
else:
raise TypeError('Expected discord.Colour or int, received %s instead.' % value.__class__.__name__)
raise TypeError('Expected discord.Colour, int, or Embed.Empty but received %s instead.' % value.__class__.__name__)
color = colour
@property
def timestamp(self):
return getattr(self, '_timestamp', None)
return getattr(self, '_timestamp', EmptyEmbed)
@timestamp.setter
def timestamp(self, value):
if isinstance(value, datetime.datetime):
if isinstance(value, (datetime.datetime, _EmptyEmbed)):
self._timestamp = value
else:
raise TypeError("Expected datetime.datetime received %s instead" % value.__class__.__name__)
raise TypeError("Expected datetime.datetime or Embed.Empty received %s instead" % value.__class__.__name__)
@property
def footer(self):
@ -173,13 +174,16 @@ class Embed:
See :meth:`set_footer` for possible values you can access.
If the attribute cannot be accessed then ``None`` is returned.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_footer', {}))
def set_footer(self, *, text=None, icon_url=None):
def set_footer(self, *, text=EmptyEmbed, icon_url=EmptyEmbed):
"""Sets the footer for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
-----------
text: str
@ -189,77 +193,79 @@ class Embed:
"""
self._footer = {}
if text is not None:
self._footer['text'] = text
if text is not EmptyEmbed:
self._footer['text'] = str(text)
if icon_url is not None:
self._footer['icon_url'] = icon_url
if icon_url is not EmptyEmbed:
self._footer['icon_url'] = str(icon_url)
return self
@property
def image(self):
"""Returns a ``EmbedProxy`` denoting the image contents.
See :meth:`set_image` for possible values you can access.
Possible attributes you can access are:
- ``url``
- ``proxy_url``
- ``width``
- ``height``
If the attribute cannot be accessed then ``None`` is returned.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_image', {}))
def set_image(self, *, url, height=None, width=None):
def set_image(self, *, url):
"""Sets the image for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
-----------
url: str
The source URL for the image. Only HTTP(S) is supported.
height: int
The height of the image.
width: int
The width of the image.
"""
self._image = {
'url': url
'url': str(url)
}
if height is not None:
self._image['height'] = height
if width is not None:
self._image['width'] = width
return self
@property
def thumbnail(self):
"""Returns a ``EmbedProxy`` denoting the thumbnail contents.
See :meth:`set_thumbnail` for possible values you can access.
Possible attributes you can access are:
- ``url``
- ``proxy_url``
- ``width``
- ``height``
If the attribute cannot be accessed then ``None`` is returned.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_thumbnail', {}))
def set_thumbnail(self, *, url, height=None, width=None):
def set_thumbnail(self, *, url):
"""Sets the thumbnail for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
-----------
url: str
The source URL for the thumbnail. Only HTTP(S) is supported.
height: int
The height of the thumbnail.
width: int
The width of the thumbnail.
"""
self._thumbnail = {
'url': url
'url': str(url)
}
if height is not None:
self._thumbnail['height'] = height
if width is not None:
self._thumbnail['width'] = width
return self
@property
def video(self):
@ -271,7 +277,7 @@ class Embed:
- ``height`` for the video height.
- ``width`` for the video width.
If the attribute cannot be accessed then ``None`` is returned.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_video', {}))
@ -281,7 +287,7 @@ class Embed:
The only attributes that might be accessed are ``name`` and ``url``.
If the attribute cannot be accessed then ``None`` is returned.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_provider', {}))
@ -291,13 +297,16 @@ class Embed:
See :meth:`set_author` for possible values you can access.
If the attribute cannot be accessed then ``None`` is returned.
If the attribute has no value then :attr:`Empty` is returned.
"""
return EmbedProxy(getattr(self, '_author', {}))
def set_author(self, *, name, url=None, icon_url=None):
def set_author(self, *, name, url=EmptyEmbed, icon_url=EmptyEmbed):
"""Sets the author for the embed content.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
-----------
name: str
@ -309,15 +318,16 @@ class Embed:
"""
self._author = {
'name': name
'name': str(name)
}
if url is not None:
self._author['url'] = url
if url is not EmptyEmbed:
self._author['url'] = str(url)
if icon_url is not None:
self._author['icon_url'] = icon_url
if icon_url is not EmptyEmbed:
self._author['icon_url'] = str(icon_url)
return self
@property
def fields(self):
@ -325,13 +335,16 @@ class Embed:
See :meth:`add_field` for possible values you can access.
If the attribute cannot be accessed then ``None`` is returned.
If the attribute has no value then :attr:`Empty` is returned.
"""
return [EmbedProxy(d) for d in getattr(self, '_fields', [])]
def add_field(self, *, name=None, value=None, inline=True):
def add_field(self, *, name, value, inline=True):
"""Adds a field to the embed object.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
-----------
name: str
@ -343,19 +356,81 @@ class Embed:
"""
field = {
'inline': inline
'inline': inline,
'name': str(name),
'value': str(value)
}
if name is not None:
field['name'] = name
if value is not None:
field['value'] = value
try:
self._fields.append(field)
except AttributeError:
self._fields = [field]
return self
def clear_fields(self):
"""Removes all fields from this embed."""
try:
self._fields.clear()
except AttributeError:
self._fields = []
def remove_field(self, index):
"""Removes a field at a specified index.
If the index is invalid or out of bounds then the error is
silently swallowed.
.. note::
When deleting a field by index, the index of the other fields
shift to fill the gap just like a regular list.
Parameters
-----------
index: int
The index of the field to remove.
"""
try:
del self._fields[index]
except (AttributeError, IndexError):
pass
def set_field_at(self, index, *, name, value, inline=True):
"""Modifies a field to the embed object.
The index must point to a valid pre-existing field.
This function returns the class instance to allow for fluent-style
chaining.
Parameters
-----------
index: int
The index of the field to modify.
name: str
The name of the field.
value: str
The value of the field.
inline: bool
Whether the field should be displayed inline.
Raises
-------
IndexError
An invalid index was provided.
"""
try:
field = self._fields[index]
except (TypeError, IndexError, AttributeError):
raise IndexError('field index out of range')
field['name'] = str(name)
field['value'] = str(value)
field['inline'] = inline
return self
def to_dict(self):
"""Converts this embed object into a dict."""
@ -373,14 +448,16 @@ class Embed:
except KeyError:
pass
else:
result['color'] = colour.value
if colour:
result['color'] = colour.value
try:
timestamp = result.pop('timestamp')
except KeyError:
pass
else:
result['timestamp'] = timestamp.isoformat()
if timestamp:
result['timestamp'] = timestamp.isoformat()
# add in the non raw attribute ones
if self.type:

Loading…
Cancel
Save