Merge branch 'V3/release/3.0.0' into V3/develop

# Conflicts:
#	redbot/cogs/mod/mod.py
This commit is contained in:
Toby Harradine
2019-01-11 16:42:42 +11:00
18 changed files with 529 additions and 503 deletions

View File

@@ -148,5 +148,5 @@ class VersionInfo:
)
__version__ = "3.0.0rc2"
__version__ = "3.0.0rc3.post1"
version_info = VersionInfo.from_str(__version__)

View File

@@ -111,7 +111,7 @@ class RedBase(commands.GroupMixin, commands.bot.BotBase, RPCMixin):
self.main_dir = bot_dir
self.cog_mgr = CogManager(paths=(str(self.main_dir / "cogs"),))
self.cog_mgr = CogManager()
super().__init__(*args, formatter=Help(), **kwargs)

View File

@@ -3,7 +3,7 @@ import pkgutil
from importlib import import_module, invalidate_caches
from importlib.machinery import ModuleSpec
from pathlib import Path
from typing import Tuple, Union, List, Optional
from typing import Union, List, Optional
import redbot.cogs
from redbot.core.utils import deduplicate_iterables
@@ -25,8 +25,6 @@ class NoSuchCog(ImportError):
Different from ImportError because some ImportErrors can happen inside cogs.
"""
pass
class CogManager:
"""Directory manager for Red's cogs.
@@ -39,30 +37,27 @@ class CogManager:
CORE_PATH = Path(redbot.cogs.__path__[0])
def __init__(self, paths: Tuple[str] = ()):
def __init__(self):
self.conf = Config.get_conf(self, 2938473984732, True)
tmp_cog_install_path = cog_data_path(self) / "cogs"
tmp_cog_install_path.mkdir(parents=True, exist_ok=True)
self.conf.register_global(paths=[], install_path=str(tmp_cog_install_path))
self._paths = [Path(p) for p in paths]
async def paths(self) -> Tuple[Path, ...]:
"""Get all currently valid path directories.
async def paths(self) -> List[Path]:
"""Get all currently valid path directories, in order of priority
Returns
-------
`tuple` of `pathlib.Path`
All valid cog paths.
List[pathlib.Path]
A list of paths where cog packages can be found. The
install path is highest priority, followed by the
user-defined paths, and the core path has the lowest
priority.
"""
conf_paths = [Path(p) for p in await self.conf.paths()]
other_paths = self._paths
all_paths = deduplicate_iterables(conf_paths, other_paths, [self.CORE_PATH])
if self.install_path not in all_paths:
all_paths.insert(0, await self.install_path())
return tuple(p.resolve() for p in all_paths if p.is_dir())
return deduplicate_iterables(
[await self.install_path()], await self.user_defined_paths(), [self.CORE_PATH]
)
async def install_path(self) -> Path:
"""Get the install path for 3rd party cogs.
@@ -73,8 +68,20 @@ class CogManager:
The path to the directory where 3rd party cogs are stored.
"""
p = Path(await self.conf.install_path())
return p.resolve()
return Path(await self.conf.install_path()).resolve()
async def user_defined_paths(self) -> List[Path]:
"""Get a list of user-defined cog paths.
All paths will be absolute and unique, in order of priority.
Returns
-------
List[pathlib.Path]
A list of user-defined paths.
"""
return list(map(Path, deduplicate_iterables(await self.conf.paths())))
async def set_install_path(self, path: Path) -> Path:
"""Set the install path for 3rd party cogs.
@@ -125,11 +132,10 @@ class CogManager:
path = Path(path)
return path
async def add_path(self, path: Union[Path, str]):
async def add_path(self, path: Union[Path, str]) -> None:
"""Add a cog path to current list.
This will ignore duplicates. Does have a side effect of removing all
invalid paths from the saved path list.
This will ignore duplicates.
Parameters
----------
@@ -156,11 +162,12 @@ class CogManager:
if path == self.CORE_PATH:
raise ValueError("Cannot add the core path as an additional path.")
async with self.conf.paths() as paths:
if not any(Path(p) == path for p in paths):
paths.append(str(path))
current_paths = await self.user_defined_paths()
if path not in current_paths:
current_paths.append(path)
await self.set_paths(current_paths)
async def remove_path(self, path: Union[Path, str]) -> Tuple[Path, ...]:
async def remove_path(self, path: Union[Path, str]) -> None:
"""Remove a path from the current paths list.
Parameters
@@ -168,20 +175,12 @@ class CogManager:
path : `pathlib.Path` or `str`
Path to remove.
Returns
-------
`tuple` of `pathlib.Path`
Tuple of new valid paths.
"""
path = self._ensure_path_obj(path).resolve()
paths = await self.user_defined_paths()
paths = [Path(p) for p in await self.conf.paths()]
if path in paths:
paths.remove(path)
await self.set_paths(paths)
return tuple(paths)
paths.remove(path)
await self.set_paths(paths)
async def set_paths(self, paths_: List[Path]):
"""Set the current paths list.
@@ -192,7 +191,7 @@ class CogManager:
List of paths to set.
"""
str_paths = [str(p) for p in paths_]
str_paths = list(map(str, paths_))
await self.conf.paths.set(str_paths)
async def _find_ext_cog(self, name: str) -> ModuleSpec:
@@ -213,9 +212,9 @@ class CogManager:
------
NoSuchCog
When no cog with the requested name was found.
"""
resolved_paths = await self.paths()
real_paths = [str(p) for p in resolved_paths if p != self.CORE_PATH]
real_paths = list(map(str, [await self.install_path()] + await self.user_defined_paths()))
for finder, module_name, _ in pkgutil.iter_modules(real_paths):
if name == module_name:
@@ -287,10 +286,8 @@ class CogManager:
return await self._find_core_cog(name)
async def available_modules(self) -> List[str]:
"""Finds the names of all available modules to load.
"""
paths = (await self.install_path(),) + await self.paths()
paths = [str(p) for p in paths]
"""Finds the names of all available modules to load."""
paths = list(map(str, await self.paths()))
ret = []
for finder, module_name, _ in pkgutil.iter_modules(paths):
@@ -314,13 +311,6 @@ _ = Translator("CogManagerUI", __file__)
class CogManagerUI(commands.Cog):
"""Commands to interface with Red's cog manager."""
@staticmethod
async def visible_paths(ctx):
install_path = await ctx.bot.cog_mgr.install_path()
cog_paths = await ctx.bot.cog_mgr.paths()
cog_paths = [p for p in cog_paths if p != install_path]
return cog_paths
@commands.command()
@checks.is_owner()
async def paths(self, ctx: commands.Context):
@@ -330,8 +320,7 @@ class CogManagerUI(commands.Cog):
cog_mgr = ctx.bot.cog_mgr
install_path = await cog_mgr.install_path()
core_path = cog_mgr.CORE_PATH
cog_paths = await cog_mgr.paths()
cog_paths = [p for p in cog_paths if p not in (install_path, core_path)]
cog_paths = await cog_mgr.user_defined_paths()
msg = _("Install Path: {install_path}\nCore Path: {core_path}\n\n").format(
install_path=install_path, core_path=core_path
@@ -369,7 +358,11 @@ class CogManagerUI(commands.Cog):
from !paths
"""
path_number -= 1
cog_paths = await self.visible_paths(ctx)
if path_number < 0:
await ctx.send(_("Path numbers must be positive."))
return
cog_paths = await ctx.bot.cog_mgr.user_defined_paths()
try:
to_remove = cog_paths.pop(path_number)
except IndexError:
@@ -388,8 +381,11 @@ class CogManagerUI(commands.Cog):
# Doing this because in the paths command they're 1 indexed
from_ -= 1
to -= 1
if from_ < 0 or to < 0:
await ctx.send(_("Path numbers must be positive."))
return
all_paths = await self.visible_paths(ctx)
all_paths = await ctx.bot.cog_mgr.user_defined_paths()
try:
to_move = all_paths.pop(from_)
except IndexError:

View File

@@ -145,7 +145,7 @@ class Command(CogCommandMixin, commands.Command):
@property
def parents(self) -> List["Group"]:
"""List[Group] : Returns all parent commands of this command.
"""List[commands.Group] : Returns all parent commands of this command.
This is sorted by the length of :attr:`.qualified_name` from highest to lowest.
If the command has no parents, this will be an empty list.

View File

@@ -1173,6 +1173,9 @@ class Core(commands.Cog, CoreLogic):
await ctx.send(
_("A backup has been made of this instance. It is at {}.").format(backup_file)
)
if backup_file.stat().st_size > 8_000_000:
await ctx.send(_("This backup is to large to send via DM."))
return
await ctx.send(_("Would you like to receive a copy via DM? (y/n)"))
pred = MessagePredicate.yes_or_no(ctx)
@@ -1183,10 +1186,18 @@ class Core(commands.Cog, CoreLogic):
else:
if pred.result is True:
await ctx.send(_("OK, it's on its way!"))
async with ctx.author.typing():
await ctx.author.send(
_("Here's a copy of the backup"), file=discord.File(str(backup_file))
try:
async with ctx.author.typing():
await ctx.author.send(
_("Here's a copy of the backup"),
file=discord.File(str(backup_file)),
)
except discord.Forbidden:
await ctx.send(
_("I don't seem to be able to DM you. Do you have closed DMs?")
)
except discord.HTTPException:
await ctx.send(_("I could not send the backup file."))
else:
await ctx.send(_("OK then."))
else:

View File

@@ -15,7 +15,7 @@ from pkg_resources import DistributionNotFound
from . import __version__ as red_version, version_info as red_version_info, VersionInfo, commands
from .data_manager import storage_type
from .utils.chat_formatting import inline, bordered, humanize_list
from .utils.chat_formatting import inline, bordered, format_perms_list
from .utils import fuzzy_command_search, format_fuzzy_results
log = logging.getLogger("red")
@@ -234,18 +234,13 @@ def init_events(bot, cli_flags):
else:
await ctx.send(await format_fuzzy_results(ctx, fuzzy_commands, embed=False))
elif isinstance(error, commands.BotMissingPermissions):
missing_perms: List[str] = []
for perm, value in error.missing:
if value is True:
perm_name = '"' + perm.replace("_", " ").title() + '"'
missing_perms.append(perm_name)
if len(missing_perms) == 1:
if bin(error.missing.value).count("1") == 1: # Only one perm missing
plural = ""
else:
plural = "s"
await ctx.send(
"I require the {perms} permission{plural} to execute that command.".format(
perms=humanize_list(missing_perms), plural=plural
perms=format_perms_list(error.missing), plural=plural
)
)
elif isinstance(error, commands.CheckFailure):

View File

@@ -1,5 +1,8 @@
import itertools
from typing import Sequence, Iterator, List
import discord
from redbot.core.i18n import Translator
_ = Translator("UtilsChatFormatting", __file__)
@@ -329,7 +332,7 @@ def escape(text: str, *, mass_mentions: bool = False, formatting: bool = False)
return text
def humanize_list(items: Sequence[str]):
def humanize_list(items: Sequence[str]) -> str:
"""Get comma-separted list, with the last element joined with *and*.
This uses an Oxford comma, because without one, items containing
@@ -357,3 +360,29 @@ def humanize_list(items: Sequence[str]):
if len(items) == 1:
return items[0]
return ", ".join(items[:-1]) + _(", and ") + items[-1]
def format_perms_list(perms: discord.Permissions) -> str:
"""Format a list of permission names.
This will return a humanized list of the names of all enabled
permissions in the provided `discord.Permissions` object.
Parameters
----------
perms : discord.Permissions
The permissions object with the requested permissions to list
enabled.
Returns
-------
str
The humanized list.
"""
perm_names: List[str] = []
for perm, value in perms:
if value is True:
perm_name = '"' + perm.replace("_", " ").title() + '"'
perm_names.append(perm_name)
return humanize_list(perm_names).replace("Guild", "Server")

View File

@@ -73,10 +73,13 @@ async def menu(
# noinspection PyAsyncCall
start_adding_reactions(message, controls.keys(), ctx.bot.loop)
else:
if isinstance(current_page, discord.Embed):
await message.edit(embed=current_page)
else:
await message.edit(content=current_page)
try:
if isinstance(current_page, discord.Embed):
await message.edit(embed=current_page)
else:
await message.edit(content=current_page)
except discord.NotFound:
return
try:
react, user = await ctx.bot.wait_for(
@@ -90,9 +93,12 @@ async def menu(
except discord.Forbidden: # cannot remove all reactions
for key in controls.keys():
await message.remove_reaction(key, ctx.bot.user)
return None
return await controls[react.emoji](ctx, pages, controls, message, page, timeout, react.emoji)
except discord.NotFound:
return
else:
return await controls[react.emoji](
ctx, pages, controls, message, page, timeout, react.emoji
)
async def next_page(
@@ -106,10 +112,8 @@ async def next_page(
):
perms = message.channel.permissions_for(ctx.me)
if perms.manage_messages: # Can manage messages, so remove react
try:
with contextlib.suppress(discord.NotFound):
await message.remove_reaction(emoji, ctx.author)
except discord.NotFound:
pass
if page == len(pages) - 1:
page = 0 # Loop around to the first item
else:
@@ -128,10 +132,8 @@ async def prev_page(
):
perms = message.channel.permissions_for(ctx.me)
if perms.manage_messages: # Can manage messages, so remove react
try:
with contextlib.suppress(discord.NotFound):
await message.remove_reaction(emoji, ctx.author)
except discord.NotFound:
pass
if page == 0:
page = len(pages) - 1 # Loop around to the last item
else:
@@ -148,9 +150,8 @@ async def close_menu(
timeout: float,
emoji: str,
):
if message:
with contextlib.suppress(discord.NotFound):
await message.delete()
return None
def start_adding_reactions(
@@ -161,7 +162,7 @@ def start_adding_reactions(
"""Start adding reactions to a message.
This is a non-blocking operation - calling this will schedule the
reactions being added, but will the calling code will continue to
reactions being added, but the calling code will continue to
execute asynchronously. There is no need to await this function.
This is particularly useful if you wish to start waiting for a
@@ -169,7 +170,7 @@ def start_adding_reactions(
this is exactly what `menu` uses to do that.
This spawns a `asyncio.Task` object and schedules it on ``loop``.
If ``loop`` omitted, the loop will be retreived with
If ``loop`` omitted, the loop will be retrieved with
`asyncio.get_event_loop`.
Parameters

View File

@@ -2,7 +2,6 @@ import discord
from datetime import datetime
from redbot.core.utils.chat_formatting import pagify
import io
import sys
import weakref
from typing import List, Optional
from .common_filters import filter_mass_mentions
@@ -151,15 +150,12 @@ class Tunnel(metaclass=TunnelMeta):
"""
files = []
size = 0
max_size = 8 * 1024 * 1024
for a in m.attachments:
_fp = io.BytesIO()
await a.save(_fp)
size += sys.getsizeof(_fp)
if size > max_size:
return []
files.append(discord.File(_fp, filename=a.filename))
max_size = 8 * 1000 * 1000
if m.attachments and sum(a.size for a in m.attachments) <= max_size:
for a in m.attachments:
_fp = io.BytesIO()
await a.save(_fp)
files.append(discord.File(_fp, filename=a.filename))
return files
async def communicate(