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:
parent
bda3afcad7
commit
23fe6b46dd
@ -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
|
||||
|
10
discord/types/__init__.py
Normal file
10
discord/types/__init__.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""
|
||||
discord.types
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Typings for the Discord API
|
||||
|
||||
:copyright: (c) 2015-present Rapptz
|
||||
:license: MIT, see LICENSE for more details.
|
||||
|
||||
"""
|
86
discord/types/common.py
Normal file
86
discord/types/common.py
Normal file
@ -0,0 +1,86 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
# This file provides typings for shared entities between the Gateway and HTTP API
|
||||
|
||||
from typing import List, Literal, TypedDict
|
||||
|
||||
class _EmbedFooterOptional(TypedDict, total=False):
|
||||
icon_url: str
|
||||
proxy_icon_url: str
|
||||
|
||||
class EmbedFooter(_EmbedFooterOptional):
|
||||
text: str
|
||||
|
||||
class _EmbedFieldOptional(TypedDict, total=False):
|
||||
inline: bool
|
||||
|
||||
class EmbedField(_EmbedFieldOptional):
|
||||
name: str
|
||||
value: str
|
||||
|
||||
class EmbedThumbnail(TypedDict, total=False):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: int
|
||||
width: int
|
||||
|
||||
class EmbedVideo(TypedDict, total=False):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: int
|
||||
width: int
|
||||
|
||||
class EmbedImage(TypedDict, total=False):
|
||||
url: str
|
||||
proxy_url: str
|
||||
height: int
|
||||
width: int
|
||||
|
||||
class EmbedProvider(TypedDict, total=False):
|
||||
name: str
|
||||
url: str
|
||||
|
||||
class EmbedAuthor(TypedDict, total=False):
|
||||
name: str
|
||||
url: str
|
||||
icon_url: str
|
||||
proxy_icon_url: str
|
||||
|
||||
EmbedType = Literal['rich', 'image', 'video', 'gifv', 'article', 'link']
|
||||
|
||||
class Embed(TypedDict, total=False):
|
||||
title: str
|
||||
type: EmbedType
|
||||
description: str
|
||||
url: str
|
||||
timestamp: str
|
||||
color: int
|
||||
footer: EmbedFooter
|
||||
image: EmbedImage
|
||||
thumbnail: EmbedThumbnail
|
||||
video: EmbedVideo
|
||||
provider: EmbedProvider
|
||||
author: EmbedAuthor
|
||||
fields: List[EmbedField]
|
2
setup.py
2
setup.py
@ -50,7 +50,7 @@ setup(name='discord.py',
|
||||
"Issue tracker": "https://github.com/Rapptz/discord.py/issues",
|
||||
},
|
||||
version=version,
|
||||
packages=['discord', 'discord.ext.commands', 'discord.ext.tasks'],
|
||||
packages=['discord', 'discord.types', 'discord.ext.commands', 'discord.ext.tasks'],
|
||||
license='MIT',
|
||||
description='A Python wrapper for the Discord API',
|
||||
long_description=readme,
|
||||
|
Loading…
x
Reference in New Issue
Block a user