Discord.py dep update 3.1 (#2587)

* Dependency update

discord.py==1.0.1
websockets<7

[style]
black==19.3b0

[Docs]
jinja==2.10.1
urllib3==1.24.2

Changes related to breaking changes from discord.py have also been made
to match

As of this commit, help formatter is back to discord.py's default
This commit is contained in:
Michael H
2019-04-23 21:40:38 -04:00
committed by GitHub
parent 0ff7259bc3
commit ad114295e7
83 changed files with 231 additions and 22307 deletions

View File

@@ -14,7 +14,7 @@ from discord.ext.commands import when_mentioned_or
from . import Config, i18n, commands, errors
from .cog_manager import CogManager
from .help_formatter import Help, help as help_
from .rpc import RPCMixin
from .utils import common_filters
@@ -29,10 +29,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
"""Mixin for the main bot class.
This exists because `Red` inherits from `discord.AutoShardedClient`, which
is something other bot classes (namely selfbots) may not want to have as
a parent class.
Selfbots should inherit from this mixin along with `discord.Client`.
is something other bot classes may not want to have as a parent class.
"""
def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
@@ -118,11 +115,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
self.cog_mgr = CogManager()
super().__init__(*args, formatter=Help(), **kwargs)
self.remove_command("help")
self.add_command(help_)
super().__init__(*args, help_command=commands.DefaultHelpCommand(), **kwargs)
self._permissions_hooks: List[commands.CheckPredicate] = []
@@ -216,6 +209,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
curr_pkgs.remove(pkg_name)
async def load_extension(self, spec: ModuleSpec):
# NB: this completely bypasses `discord.ext.commands.Bot._load_from_module_spec`
name = spec.name.split(".")[-1]
if name in self.extensions:
raise errors.PackageAlreadyLoaded(spec)
@@ -225,12 +219,17 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
del lib
raise discord.ClientException(f"extension {name} does not have a setup function")
if asyncio.iscoroutinefunction(lib.setup):
await lib.setup(self)
try:
if asyncio.iscoroutinefunction(lib.setup):
await lib.setup(self)
else:
lib.setup(self)
except Exception as e:
self._remove_module_references(lib.__name__)
self._call_module_finalizers(lib, key)
raise errors.ExtensionFailed(key, e) from e
else:
lib.setup(self)
self.extensions[name] = lib
self._BotBase__extensions[name] = lib
def remove_cog(self, cogname: str):
cog = self.get_cog(cogname)
@@ -250,62 +249,6 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
for meth in self.rpc_handlers.pop(cogname.upper(), ()):
self.unregister_rpc_handler(meth)
def unload_extension(self, name):
lib = self.extensions.get(name)
if lib is None:
return
lib_name = lib.__name__ # Thank you
# find all references to the module
# remove the cogs registered from the module
for cogname, cog in self.cogs.copy().items():
if cog.__module__ and _is_submodule(lib_name, cog.__module__):
self.remove_cog(cogname)
# first remove all the commands from the module
for cmd in self.all_commands.copy().values():
if cmd.module and _is_submodule(lib_name, cmd.module):
if isinstance(cmd, discord.ext.commands.GroupMixin):
cmd.recursively_remove_all_commands()
self.remove_command(cmd.name)
# then remove all the listeners from the module
for event_list in self.extra_events.copy().values():
remove = []
for index, event in enumerate(event_list):
if event.__module__ and _is_submodule(lib_name, event.__module__):
remove.append(index)
for index in reversed(remove):
del event_list[index]
try:
func = getattr(lib, "teardown")
except AttributeError:
pass
else:
try:
func(self)
except:
pass
finally:
# finally remove the import..
pkg_name = lib.__package__
del lib
del self.extensions[name]
for module in list(sys.modules):
if _is_submodule(lib_name, module):
del sys.modules[module]
if pkg_name.startswith("redbot.cogs."):
del sys.modules["redbot.cogs"].__dict__[name]
async def is_automod_immune(
self, to_check: Union[discord.Message, commands.Context, discord.abc.User, discord.Role]
) -> bool:
@@ -399,11 +342,9 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
else:
self.add_permissions_hook(hook)
for attr in dir(cog):
_attr = getattr(cog, attr)
if isinstance(_attr, discord.ext.commands.Command) and not isinstance(
_attr, commands.Command
):
for command in cog.__cog_commands__:
if not isinstance(command, commands.Command):
raise RuntimeError(
f"The {cog.__class__.__name__} cog in the {cog.__module__} package,"
" is not using Red's command module, and cannot be added. "
@@ -414,13 +355,8 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
)
super().add_cog(cog)
self.dispatch("cog_add", cog)
def add_command(self, command: commands.Command):
if not isinstance(command, commands.Command):
raise TypeError("Command objects must derive from redbot.core.commands.Command")
super().add_command(command)
self.dispatch("command_add", command)
for command in cog.__cog_commands__:
self.dispatch("command_add", command)
def clear_permission_rules(self, guild_id: Optional[int]) -> None:
"""Clear all permission overrides in a scope.

View File

@@ -4,3 +4,4 @@ from .context import *
from .converter import *
from .errors import *
from .requires import *
from .help import *

View File

@@ -20,6 +20,7 @@ if TYPE_CHECKING:
__all__ = [
"Cog",
"CogMixin",
"CogCommandMixin",
"CogGroupMixin",
"Command",
@@ -241,9 +242,9 @@ class Command(CogCommandMixin, commands.Command):
if result is False:
return False
if self.parent is None and self.instance is not None:
if self.parent is None and self.cog is not None:
# For top-level commands, we need to check the cog's requires too
ret = await self.instance.requires.verify(ctx)
ret = await self.cog.requires.verify(ctx)
if ret is False:
return False
@@ -374,8 +375,8 @@ class Command(CogCommandMixin, commands.Command):
def allow_for(self, model_id: Union[int, str], guild_id: int) -> None:
super().allow_for(model_id, guild_id=guild_id)
parents = self.parents
if self.instance is not None:
parents.append(self.instance)
if self.cog is not None:
parents.append(self.cog)
for parent in parents:
cur_rule = parent.requires.get_rule(model_id, guild_id=guild_id)
if cur_rule is PermState.NORMAL:
@@ -389,8 +390,8 @@ class Command(CogCommandMixin, commands.Command):
old_rule, new_rule = super().clear_rule_for(model_id, guild_id=guild_id)
if old_rule is PermState.ACTIVE_ALLOW:
parents = self.parents
if self.instance is not None:
parents.append(self.instance)
if self.cog is not None:
parents.append(self.cog)
for parent in parents:
should_continue = parent.reevaluate_rules_for(model_id, guild_id=guild_id)[1]
if not should_continue:
@@ -445,10 +446,11 @@ class GroupMixin(discord.ext.commands.GroupMixin):
def command(self, *args, **kwargs):
"""A shortcut decorator that invokes :func:`.command` and adds it to
the internal command list.
the internal command list via :meth:`~.GroupMixin.add_command`.
"""
def decorator(func):
kwargs.setdefault("parent", self)
result = command(*args, **kwargs)(func)
self.add_command(result)
return result
@@ -457,10 +459,11 @@ class GroupMixin(discord.ext.commands.GroupMixin):
def group(self, *args, **kwargs):
"""A shortcut decorator that invokes :func:`.group` and adds it to
the internal command list.
the internal command list via :meth:`~.GroupMixin.add_command`.
"""
def decorator(func):
kwargs.setdefault("parent", self)
result = group(*args, **kwargs)(func)
self.add_command(result)
return result
@@ -551,12 +554,24 @@ class Group(GroupMixin, Command, CogGroupMixin, commands.Group):
await super().invoke(ctx)
class Cog(CogCommandMixin, CogGroupMixin):
"""Base class for a cog."""
class CogMixin(CogGroupMixin, CogCommandMixin):
"""Mixin class for a cog, intended for use with discord.py's cog class"""
@property
def all_commands(self) -> Dict[str, Command]:
return {cmd.name: cmd for cmd in self.__dict__.values() if isinstance(cmd, Command)}
return {cmd.name: cmd for cmd in self.__cog_commands__}
class Cog(CogMixin, commands.Cog):
"""
Red's Cog base class
This includes a metaclass from discord.py
"""
# NB: Do not move the inheritcance of this. Keeping the mix of that metaclass
# seperate gives us more freedoms in several places.
pass
def command(name=None, cls=Command, **attrs):

View File

@@ -63,44 +63,9 @@ class Context(commands.Context):
return await super().send(content=content, **kwargs)
async def send_help(self) -> List[discord.Message]:
"""Send the command help message.
Returns
-------
`list` of `discord.Message`
A list of help messages which were sent to the user.
"""
""" Send the command help message. """
command = self.invoked_subcommand or self.command
embed_wanted = await self.bot.embed_requested(
self.channel, self.author, command=self.bot.get_command("help")
)
if self.guild and not self.channel.permissions_for(self.guild.me).embed_links:
embed_wanted = False
ret = []
destination = self
if embed_wanted:
embeds = await self.bot.formatter.format_help_for(self, command)
for embed in embeds:
try:
m = await destination.send(embed=embed)
except discord.HTTPException:
destination = self.author
m = await destination.send(embed=embed)
ret.append(m)
else:
f = commands.HelpFormatter()
msgs = await f.format_help_for(self, command)
for msg in msgs:
try:
m = await destination.send(msg)
except discord.HTTPException:
destination = self.author
m = await destination.send(msg)
ret.append(m)
return ret
await super().send_help(command)
async def tick(self) -> bool:
"""Add a tick reaction to the command message.

View File

@@ -0,0 +1,23 @@
from discord.ext import commands
from .commands import Command
__all__ = ["HelpCommand", "DefaultHelpCommand", "MinimalHelpCommand"]
class _HelpCommandImpl(Command, commands.help._HelpCommandImpl):
pass
class HelpCommand(commands.help.HelpCommand):
def _add_to_bot(self, bot):
command = _HelpCommandImpl(self, self.command_callback, **self.command_attrs)
bot.add_command(command)
self._command_impl = command
class DefaultHelpCommand(HelpCommand, commands.help.DefaultHelpCommand):
pass
class MinimalHelpCommand(HelpCommand, commands.help.MinimalHelpCommand):
pass

View File

@@ -119,7 +119,7 @@ def init_events(bot, cli_flags):
"Outdated version! {} is available "
"but you're using {}".format(data["info"]["version"], red_version)
)
owner = await bot.get_user_info(bot.owner_id)
owner = await bot.fetch_user(bot.owner_id)
await owner.send(
"Your Red instance is out of date! {} is the current "
"version, however you are using {}!".format(
@@ -168,8 +168,9 @@ def init_events(bot, cli_flags):
if hasattr(ctx.command, "on_error"):
return
if ctx.cog and hasattr(ctx.cog, f"_{ctx.cog.__class__.__name__}__error"):
return
if ctx.cog:
if commands.Cog._get_overridden_method(ctx.cog.cog_command_error) is not None:
return
if isinstance(error, commands.MissingRequiredArgument):
await ctx.send_help()

View File

@@ -1,403 +0,0 @@
"""Overrides the built-in help formatter.
All help messages will be embed and pretty.
Most of the code stolen from
discord.ext.commands.formatter.py and
converted into embeds instead of codeblocks.
Docstr on cog class becomes category.
Docstr on command definition becomes command
summary and usage.
Use [p] in command docstr for bot prefix.
See [p]help here for example.
await bot.formatter.format_help_for(ctx, command)
to send help page for command. Optionally pass a
string as third arg to add a more descriptive
message to help page.
e.g. format_help_for(ctx, ctx.command, "Missing required arguments")
discord.py 1.0.0a
This help formatter contains work by Rapptz (Danny) and SirThane#1780.
"""
import contextlib
from collections import namedtuple
from typing import List, Optional, Union
import discord
from discord.ext.commands import formatter as dpy_formatter
import inspect
import itertools
import re
from . import commands
from .i18n import Translator
from .utils.chat_formatting import pagify
from .utils import fuzzy_command_search, format_fuzzy_results
_ = Translator("Help", __file__)
EMPTY_STRING = "\u200b"
_mentions_transforms = {"@everyone": "@\u200beveryone", "@here": "@\u200bhere"}
_mention_pattern = re.compile("|".join(_mentions_transforms.keys()))
EmbedField = namedtuple("EmbedField", "name value inline")
class Help(dpy_formatter.HelpFormatter):
"""Formats help for commands."""
def __init__(self, *args, **kwargs):
self.context = None
self.command = None
super().__init__(*args, **kwargs)
@staticmethod
def pm_check(ctx):
return isinstance(ctx.channel, discord.DMChannel)
@property
def me(self):
return self.context.me
@property
def bot_all_commands(self):
return self.context.bot.all_commands
@property
def avatar(self):
return self.context.bot.user.avatar_url_as(format="png")
async def color(self):
if self.pm_check(self.context):
return self.context.bot.color
else:
return await self.context.embed_colour()
colour = color
@property
def destination(self):
if self.context.bot.pm_help:
return self.context.author
return self.context
# All the other shit
@property
def author(self):
# Get author dict with username if PM and display name in guild
if self.pm_check(self.context):
name = self.context.bot.user.name
else:
name = self.me.display_name if not "" else self.context.bot.user.name
author = {"name": "{0} Help Manual".format(name), "icon_url": self.avatar}
return author
def _add_subcommands(self, cmds):
entries = ""
for name, command in cmds:
if name in command.aliases:
# skip aliases
continue
if self.is_cog() or self.is_bot():
name = "{0}{1}".format(self.context.clean_prefix, name)
entries += "**{0}** {1}\n".format(name, command.short_doc)
return entries
def get_ending_note(self):
# command_name = self.context.invoked_with
return (
"Type {0}help <command> for more info on a command. "
"You can also type {0}help <category> for more info on a category.".format(
self.context.clean_prefix
)
)
async def format(self) -> dict:
"""Formats command for output.
Returns a dict used to build embed"""
emb = {"embed": {"title": "", "description": ""}, "footer": {"text": ""}, "fields": []}
if self.is_cog():
translator = getattr(self.command, "__translator__", lambda s: s)
description = (
inspect.cleandoc(translator(self.command.__doc__))
if self.command.__doc__
else EMPTY_STRING
)
else:
description = self.command.description
if not description == "" and description is not None:
description = "*{0}*".format(description)
if description:
# <description> portion
emb["embed"]["description"] = description[:2046]
tagline = await self.context.bot.db.help.tagline()
if tagline:
footer = tagline
else:
footer = self.get_ending_note()
emb["footer"]["text"] = footer
if isinstance(self.command, discord.ext.commands.core.Command):
# <signature portion>
emb["embed"]["title"] = emb["embed"]["description"]
emb["embed"]["description"] = "`Syntax: {0}`".format(self.get_command_signature())
# <long doc> section
if self.command.help:
splitted = self.command.help.split("\n\n")
name = "__{0}__".format(splitted[0])
value = "\n\n".join(splitted[1:]).replace("[p]", self.context.clean_prefix)
if value == "":
value = EMPTY_STRING
field = EmbedField(name[:252], value[:1024], False)
emb["fields"].append(field)
# end it here if it's just a regular command
if not self.has_subcommands():
return emb
def category(tup):
# Turn get cog (Category) name from cog/list tuples
cog = tup[1].cog_name
return "**__{0}:__**".format(cog) if cog is not None else "**__\u200bNo Category:__**"
# Get subcommands for bot or category
filtered = await self.filter_command_list()
if self.is_bot():
# Get list of non-hidden commands for bot.
data = sorted(filtered, key=category)
for category, commands_ in itertools.groupby(data, key=category):
commands_ = sorted(commands_)
if len(commands_) > 0:
for i, page in enumerate(
pagify(self._add_subcommands(commands_), page_length=1000)
):
title = category if i < 1 else f"{category} (continued)"
field = EmbedField(title, page, False)
emb["fields"].append(field)
else:
# Get list of commands for category
filtered = sorted(filtered)
if filtered:
for i, page in enumerate(
pagify(self._add_subcommands(filtered), page_length=1000)
):
title = (
"**__Commands:__**"
if not self.is_bot() and self.is_cog()
else "**__Subcommands:__**"
)
if i > 0:
title += " (continued)"
field = EmbedField(title, page, False)
emb["fields"].append(field)
return emb
@staticmethod
def group_fields(fields: List[EmbedField], max_chars=1000):
curr_group = []
ret = []
for f in fields:
if sum(len(f2.value) for f2 in curr_group) + len(f.value) > max_chars and curr_group:
ret.append(curr_group)
curr_group = []
curr_group.append(f)
if len(curr_group) > 0:
ret.append(curr_group)
return ret
async def format_help_for(self, ctx, command_or_bot, reason: str = ""):
"""Formats the help page and handles the actual heavy lifting of how
the help command looks like. To change the behaviour, override the
:meth:`~.HelpFormatter.format` method.
Parameters
-----------
ctx: :class:`.Context`
The context of the invoked help command.
command_or_bot: :class:`.Command` or :class:`.Bot`
The bot or command that we are getting the help of.
reason : str
Returns
--------
list
A paginated output of the help command.
"""
self.context = ctx
self.command = command_or_bot
# We want the permission state to be set as if the author had run the command he is
# requesting help for. This is so the subcommands shown in the help menu correctly reflect
# any permission rules set.
if isinstance(self.command, commands.Command):
with contextlib.suppress(commands.CommandError):
await self.command.can_run(
self.context, check_all_parents=True, change_permission_state=True
)
elif isinstance(self.command, commands.Cog):
with contextlib.suppress(commands.CommandError):
# Cog's don't have a `can_run` method, so we use the `Requires` object directly.
await self.command.requires.verify(self.context)
emb = await self.format()
if reason:
emb["embed"]["title"] = reason
ret = []
page_char_limit = await ctx.bot.db.help.page_char_limit()
field_groups = self.group_fields(emb["fields"], page_char_limit)
for i, group in enumerate(field_groups, 1):
embed = discord.Embed(color=await self.color(), **emb["embed"])
if len(field_groups) > 1:
description = "{} *- Page {} of {}*".format(
embed.description, i, len(field_groups)
)
embed.description = description
embed.set_author(**self.author)
for field in group:
embed.add_field(**field._asdict())
embed.set_footer(**emb["footer"])
ret.append(embed)
return ret
async def format_command_not_found(
self, ctx: commands.Context, command_name: str
) -> Optional[Union[str, discord.Message]]:
"""Get the response for a user calling help on a missing command."""
self.context = ctx
return await default_command_not_found(
ctx,
command_name,
use_embeds=True,
colour=await self.colour(),
author=self.author,
footer={"text": self.get_ending_note()},
)
@commands.command(hidden=True)
async def help(ctx: commands.Context, *, command_name: str = ""):
"""Show help documentation.
- `[p]help`: Show the help manual.
- `[p]help command`: Show help for a command.
- `[p]help Category`: Show commands and description for a category,
"""
bot = ctx.bot
if bot.pm_help:
destination = ctx.author
else:
destination = ctx.channel
use_embeds = await ctx.embed_requested()
if use_embeds:
formatter = bot.formatter
else:
formatter = dpy_formatter.HelpFormatter()
if not command_name:
# help by itself just lists our own commands.
pages = await formatter.format_help_for(ctx, bot)
else:
# First check if it's a cog
command = bot.get_cog(command_name)
if command is None:
command = bot.get_command(command_name)
if command is None:
if hasattr(formatter, "format_command_not_found"):
msg = await formatter.format_command_not_found(ctx, command_name)
else:
msg = await default_command_not_found(ctx, command_name, use_embeds=use_embeds)
pages = [msg]
else:
pages = await formatter.format_help_for(ctx, command)
max_pages_in_guild = await ctx.bot.db.help.max_pages_in_guild()
if len(pages) > max_pages_in_guild:
destination = ctx.author
if ctx.guild and not ctx.guild.me.permissions_in(ctx.channel).send_messages:
destination = ctx.author
try:
for page in pages:
if isinstance(page, discord.Embed):
await destination.send(embed=page)
else:
await destination.send(page)
except discord.Forbidden:
await ctx.channel.send(
_(
"I couldn't send the help message to you in DM. Either you blocked me or you "
"disabled DMs in this server."
)
)
async def default_command_not_found(
ctx: commands.Context, command_name: str, *, use_embeds: bool, **embed_options
) -> Optional[Union[str, discord.Embed]]:
"""Default function for formatting the response to a missing command."""
ret = None
cmds = command_name.split()
prev_command = None
for invoked in itertools.accumulate(cmds, lambda *args: " ".join(args)):
command = ctx.bot.get_command(invoked)
if command is None:
if prev_command is not None and not isinstance(prev_command, commands.Group):
ret = _("Command *{command_name}* has no subcommands.").format(
command_name=prev_command.qualified_name
)
break
elif not await command.can_see(ctx):
return
prev_command = command
if ret is None:
fuzzy_commands = await fuzzy_command_search(ctx, command_name, min_score=75)
if fuzzy_commands:
ret = await format_fuzzy_results(ctx, fuzzy_commands, embed=use_embeds)
else:
ret = _("Command *{command_name}* not found.").format(command_name=command_name)
if use_embeds:
if isinstance(ret, str):
ret = discord.Embed(title=ret)
if "colour" in embed_options:
ret.colour = embed_options.pop("colour")
elif "color" in embed_options:
ret.colour = embed_options.pop("color")
if "author" in embed_options:
ret.set_author(**embed_options.pop("author"))
if "footer" in embed_options:
ret.set_footer(**embed_options.pop("footer"))
return ret

View File

@@ -249,10 +249,10 @@ class Case:
guild = mod_channel.guild
if data["message"]:
try:
message = await mod_channel.get_message(data["message"])
message = await mod_channel.fetch_message(data["message"])
except discord.NotFound:
message = None
user = await bot.get_user_info(data["user"])
user = await bot.fetch_user(data["user"])
moderator = guild.get_member(data["moderator"])
channel = guild.get_channel(data["channel"])
amended_by = guild.get_member(data["amended_by"])
@@ -489,7 +489,7 @@ async def get_cases_for_member(
if not member:
member = guild.get_member(member_id)
if not member:
member = await bot.get_user_info(member_id)
member = await bot.fetch_user(member_id)
try:
mod_channel = await get_modlog_channel(guild)
@@ -501,7 +501,7 @@ async def get_cases_for_member(
message = None
if data["message"] and mod_channel:
try:
message = await mod_channel.get_message(data["message"])
message = await mod_channel.fetch_message(data["message"])
except discord.NotFound:
pass