Merge changes for RC1 into V3/develop

This commit is contained in:
Toby Harradine 2018-10-07 12:06:28 +11:00 committed by GitHub
commit 748847d5bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 213 additions and 72 deletions

View File

@ -28,7 +28,7 @@ jobs:
- python: 3.6.6 - python: 3.6.6
env: TOXENV=style env: TOXENV=style
# These jobs only occur on tag creation for V3/develop if the prior ones succeed # These jobs only occur on tag creation if the prior ones succeed
- stage: PyPi Deployment - stage: PyPi Deployment
if: tag IS present if: tag IS present
python: 3.6.6 python: 3.6.6
@ -42,7 +42,6 @@ jobs:
skip_cleanup: true skip_cleanup: true
on: on:
repo: Cog-Creators/Red-DiscordBot repo: Cog-Creators/Red-DiscordBot
branch: V3/develop
python: 3.6.6 python: 3.6.6
tags: true tags: true
- stage: Crowdin Deployment - stage: Crowdin Deployment
@ -62,6 +61,5 @@ jobs:
skip_cleanup: true skip_cleanup: true
on: on:
repo: Cog-Creators/Red-DiscordBot repo: Cog-Creators/Red-DiscordBot
branch: V3/develop
python: 3.6.6 python: 3.6.6
tags: true tags: true

View File

@ -359,7 +359,7 @@ class Admin(commands.Cog):
selfroles = await self._valid_selfroles(ctx.guild) selfroles = await self._valid_selfroles(ctx.guild)
fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles]) fmt_selfroles = "\n".join(["+ " + r.name for r in selfroles])
msg = _("Available Selfroles: {selfroles}").format(selfroles=fmt_selfroles) msg = _("Available Selfroles:\n{selfroles}").format(selfroles=fmt_selfroles)
await ctx.send(box(msg, "diff")) await ctx.send(box(msg, "diff"))
async def _serverlock_check(self, guild: discord.Guild) -> bool: async def _serverlock_check(self, guild: discord.Guild) -> bool:

View File

@ -34,7 +34,7 @@ async def download_lavalink(session):
async def maybe_download_lavalink(loop, cog): async def maybe_download_lavalink(loop, cog):
jar_exists = LAVALINK_JAR_FILE.exists() jar_exists = LAVALINK_JAR_FILE.exists()
current_build = redbot.core.VersionInfo(*await cog.config.current_build()) current_build = redbot.core.VersionInfo.from_json(await cog.config.current_build())
if not jar_exists or current_build < redbot.core.version_info: if not jar_exists or current_build < redbot.core.version_info:
log.info("Downloading Lavalink.jar") log.info("Downloading Lavalink.jar")

View File

@ -253,7 +253,9 @@ class Audio(commands.Cog):
dj_enabled = await self.config.guild(ctx.guild).dj_enabled() dj_enabled = await self.config.guild(ctx.guild).dj_enabled()
await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled) await self.config.guild(ctx.guild).dj_enabled.set(not dj_enabled)
await self._embed_msg(ctx, "DJ role enabled: {}.".format(not dj_enabled)) await self._embed_msg(
ctx, _("DJ role enabled: {true_or_false}.".format(true_or_false=not dj_enabled))
)
@audioset.command() @audioset.command()
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@ -332,7 +334,7 @@ class Audio(commands.Cog):
jarbuild = redbot.core.__version__ jarbuild = redbot.core.__version__
vote_percent = data["vote_percent"] vote_percent = data["vote_percent"]
msg = "----" + _("Server Settings") + "----" msg = "----" + _("Server Settings") + "----\n"
if emptydc_enabled: if emptydc_enabled:
msg += _("Disconnect timer: [{num_seconds}]\n").format( msg += _("Disconnect timer: [{num_seconds}]\n").format(
num_seconds=self._dynamic_time(emptydc_timer) num_seconds=self._dynamic_time(emptydc_timer)
@ -370,7 +372,9 @@ class Audio(commands.Cog):
"""Toggle displaying a thumbnail on audio messages.""" """Toggle displaying a thumbnail on audio messages."""
thumbnail = await self.config.guild(ctx.guild).thumbnail() thumbnail = await self.config.guild(ctx.guild).thumbnail()
await self.config.guild(ctx.guild).thumbnail.set(not thumbnail) await self.config.guild(ctx.guild).thumbnail.set(not thumbnail)
await self._embed_msg(ctx, _("Thumbnail display: {}.").format(not thumbnail)) await self._embed_msg(
ctx, _("Thumbnail display: {true_or_false}.").format(true_or_false=not thumbnail)
)
@audioset.command() @audioset.command()
@checks.mod_or_permissions(administrator=True) @checks.mod_or_permissions(administrator=True)
@ -565,6 +569,8 @@ class Audio(commands.Cog):
if dj_enabled: if dj_enabled:
if not await self._can_instaskip(ctx, ctx.author): if not await self._can_instaskip(ctx, ctx.author):
return await menu(ctx, folder_page_list, DEFAULT_CONTROLS) return await menu(ctx, folder_page_list, DEFAULT_CONTROLS)
else:
await menu(ctx, folder_page_list, LOCAL_FOLDER_CONTROLS)
else: else:
await menu(ctx, folder_page_list, LOCAL_FOLDER_CONTROLS) await menu(ctx, folder_page_list, LOCAL_FOLDER_CONTROLS)
@ -1097,7 +1103,7 @@ class Audio(commands.Cog):
( (
bold(playlist_name), bold(playlist_name),
_("Tracks: {num}").format(num=len(tracks)), _("Tracks: {num}").format(num=len(tracks)),
_("Author: {name}").format(self.bot.get_user(author)), _("Author: {name}\n").format(name=self.bot.get_user(author)),
) )
) )
) )
@ -1533,9 +1539,9 @@ class Audio(commands.Cog):
) )
else: else:
queue_list += _("Playing: ") queue_list += _("Playing: ")
queue_list += "**[{current.title}]({current.uri})**\n".format(current=player.current) queue_list += "**[{current.title}]({current.uri})**\n".format(current=player.current)
queue_list += _("Requested by: **{user}**").format(user=player.current.requester) queue_list += _("Requested by: **{user}**").format(user=player.current.requester)
queue_list += f"\n\n{arrow}`{pos}`/`{dur}`\n\n" queue_list += f"\n\n{arrow}`{pos}`/`{dur}`\n\n"
for i, track in enumerate( for i, track in enumerate(
player.queue[queue_idx_start:queue_idx_end], start=queue_idx_start player.queue[queue_idx_start:queue_idx_end], start=queue_idx_start
@ -2360,7 +2366,7 @@ class Audio(commands.Cog):
if await self._check_external(): if await self._check_external():
embed = discord.Embed( embed = discord.Embed(
colour=await ctx.embed_colour(), colour=await ctx.embed_colour(),
title=_("Websocket port set to {}.").format(ws_port), title=_("Websocket port set to {port}.").format(port=ws_port),
) )
embed.set_footer(text=_("External lavalink server set to True.")) embed.set_footer(text=_("External lavalink server set to True."))
await ctx.send(embed=embed) await ctx.send(embed=embed)

View File

@ -94,7 +94,7 @@ class Cleanup(commands.Cog):
): ):
if message.created_at < two_weeks_ago: if message.created_at < two_weeks_ago:
break break
if check(message): if message_filter(message):
collected.append(message) collected.append(message)
if number and number <= len(collected): if number and number <= len(collected):
break break

View File

@ -218,7 +218,7 @@ class Economy(commands.Cog):
else: else:
await bank.set_balance(to, creds.sum) await bank.set_balance(to, creds.sum)
await ctx.send( await ctx.send(
_("{author} set {users}'s account balance to {num} {currency}.").format( _("{author} set {user}'s account balance to {num} {currency}.").format(
author=author.display_name, author=author.display_name,
num=creds.sum, num=creds.sum,
currency=currency, currency=currency,

View File

@ -748,7 +748,8 @@ class Mod(commands.Cog):
to send the newly unbanned user to send the newly unbanned user
:returns: :class:`Invite`""" :returns: :class:`Invite`"""
guild = ctx.guild guild = ctx.guild
if guild.me.permissions.manage_guild: my_perms: discord.Permissions = guild.me.guild_permissions
if my_perms.manage_guild or my_perms.administrator:
if "VANITY_URL" in guild.features: if "VANITY_URL" in guild.features:
# guild has a vanity url so use it as the one to send # guild has a vanity url so use it as the one to send
return await guild.vanity_invite() return await guild.vanity_invite()

View File

@ -1,40 +1,152 @@
import re as _re
from math import inf as _inf
from typing import (
Any as _Any,
ClassVar as _ClassVar,
Dict as _Dict,
List as _List,
Optional as _Optional,
Pattern as _Pattern,
Tuple as _Tuple,
Union as _Union,
)
from .config import Config from .config import Config
__all__ = ["Config", "__version__"] __all__ = ["Config", "__version__", "version_info", "VersionInfo"]
class VersionInfo: class VersionInfo:
def __init__(self, major, minor, micro, releaselevel, serial): ALPHA = "alpha"
self._levels = ["alpha", "beta", "final"] BETA = "beta"
self.major = major RELEASE_CANDIDATE = "release candidate"
self.minor = minor FINAL = "final"
self.micro = micro
if releaselevel not in self._levels: _VERSION_STR_PATTERN: _ClassVar[_Pattern[str]] = _re.compile(
raise TypeError("'releaselevel' must be one of: {}".format(", ".join(self._levels))) r"^"
r"(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<micro>0|[1-9]\d*)"
r"(?:(?P<releaselevel>a|b|rc)(?P<serial>0|[1-9]\d*))?"
r"(?:\.post(?P<post_release>0|[1-9]\d*))?"
r"(?:\.dev(?P<dev_release>0|[1-9]\d*))?"
r"$",
flags=_re.IGNORECASE,
)
_RELEASE_LEVELS: _ClassVar[_List[str]] = [ALPHA, BETA, RELEASE_CANDIDATE, FINAL]
_SHORT_RELEASE_LEVELS: _ClassVar[_Dict[str, str]] = {
"a": ALPHA,
"b": BETA,
"rc": RELEASE_CANDIDATE,
}
self.releaselevel = releaselevel def __init__(
self.serial = serial self,
major: int,
minor: int,
micro: int,
releaselevel: str,
serial: _Optional[int] = None,
post_release: _Optional[int] = None,
dev_release: _Optional[int] = None,
) -> None:
self.major: int = major
self.minor: int = minor
self.micro: int = micro
def __lt__(self, other): if releaselevel not in self._RELEASE_LEVELS:
my_index = self._levels.index(self.releaselevel) raise TypeError(f"'releaselevel' must be one of: {', '.join(self._RELEASE_LEVELS)}")
other_index = self._levels.index(other.releaselevel)
return (self.major, self.minor, self.micro, my_index, self.serial) < ( self.releaselevel: str = releaselevel
other.major, self.serial: _Optional[int] = serial
other.minor, self.post_release: _Optional[int] = post_release
other.micro, self.dev_release: _Optional[int] = dev_release
other_index,
other.serial, @classmethod
def from_str(cls, version_str: str) -> "VersionInfo":
"""Parse a string into a VersionInfo object.
Raises
------
ValueError
If the version info string is invalid.
"""
match = cls._VERSION_STR_PATTERN.match(version_str)
if not match:
raise ValueError(f"Invalid version string: {version_str}")
kwargs: _Dict[str, _Union[str, int]] = {}
for key in ("major", "minor", "micro"):
kwargs[key] = int(match[key])
releaselevel = match["releaselevel"]
if releaselevel is not None:
kwargs["releaselevel"] = cls._SHORT_RELEASE_LEVELS[releaselevel]
else:
kwargs["releaselevel"] = cls.FINAL
for key in ("serial", "post_release", "dev_release"):
if match[key] is not None:
kwargs[key] = int(match[key])
return cls(**kwargs)
@classmethod
def from_json(
cls, data: _Union[_Dict[str, _Union[int, str]], _List[_Union[int, str]]]
) -> "VersionInfo":
if isinstance(data, _List):
# For old versions, data was stored as a list:
# [MAJOR, MINOR, MICRO, RELEASELEVEL, SERIAL]
return cls(*data)
else:
return cls(**data)
def to_json(self) -> _Dict[str, _Union[int, str]]:
return {
"major": self.major,
"minor": self.minor,
"micro": self.micro,
"releaselevel": self.releaselevel,
"serial": self.serial,
"post_release": self.post_release,
"dev_release": self.dev_release,
}
def __lt__(self, other: _Any) -> bool:
if not isinstance(other, VersionInfo):
return NotImplemented
tups: _List[_Tuple[int, int, int, int, int, int, int]] = []
for obj in (self, other):
tups.append(
(
obj.major,
obj.minor,
obj.micro,
obj._RELEASE_LEVELS.index(obj.releaselevel),
obj.serial if obj.serial is not None else _inf,
obj.post_release if obj.post_release is not None else -_inf,
obj.dev_release if obj.dev_release is not None else _inf,
)
)
return tups[0] < tups[1]
def __str__(self) -> str:
ret = f"{self.major}.{self.minor}.{self.micro}"
if self.releaselevel != self.FINAL:
short = next(
k for k, v in self._SHORT_RELEASE_LEVELS.items() if v == self.releaselevel
)
ret += f"{short}{self.serial}"
if self.post_release is not None:
ret += f".post{self.post_release}"
if self.dev_release is not None:
ret += f".dev{self.dev_release}"
return ret
def __repr__(self) -> str:
return (
"VersionInfo(major={major}, minor={minor}, micro={micro}, "
"releaselevel={releaselevel}, serial={serial}, post={post_release}, "
"dev={dev_release})".format(**self.to_json())
) )
def __repr__(self):
return "VersionInfo(major={}, minor={}, micro={}, releaselevel={}, serial={})".format(
self.major, self.minor, self.micro, self.releaselevel, self.serial
)
def to_json(self): __version__ = "3.0.0rc1.post1"
return [self.major, self.minor, self.micro, self.releaselevel, self.serial] version_info = VersionInfo.from_str(__version__)
__version__ = "3.0.0b21"
version_info = VersionInfo(3, 0, 0, "beta", 21)

View File

@ -50,12 +50,10 @@ def interactive_config(red, token_set, prefix_set):
def ask_sentry(red: Red): def ask_sentry(red: Red):
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
print( print(
"\nThank you for installing Red V3 beta! The current version\n" "\nThank you for installing Red V3! Red is constantly undergoing\n"
" is not suited for production use and is aimed at testing\n" " improvements, and we would like ask if you are comfortable with\n"
" the current and upcoming featureset, that's why we will\n" " the bot automatically submitting fatal error logs to the development\n"
" also collect the fatal error logs to help us fix any new\n" ' team. If you wish to opt into the process please type "yes":\n'
" found issues in a timely manner. If you wish to opt in\n"
' the process please type "yes":\n'
) )
if not confirm("> "): if not confirm("> "):
loop.run_until_complete(red.db.enable_sentry.set(False)) loop.run_until_complete(red.db.enable_sentry.set(False))

View File

@ -13,17 +13,20 @@ from collections import namedtuple
from pathlib import Path from pathlib import Path
from random import SystemRandom from random import SystemRandom
from string import ascii_letters, digits from string import ascii_letters, digits
from distutils.version import StrictVersion
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING, Union
import aiohttp import aiohttp
import discord import discord
import pkg_resources import pkg_resources
from redbot.core import __version__ from redbot.core import (
from redbot.core import checks __version__,
from redbot.core import i18n version_info as red_version_info,
from redbot.core import commands VersionInfo,
checks,
commands,
i18n,
)
from .utils.predicates import MessagePredicate from .utils.predicates import MessagePredicate
from .utils.chat_formatting import pagify, box, inline from .utils.chat_formatting import pagify, box, inline
@ -274,7 +277,7 @@ class Core(commands.Cog, CoreLogic):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get("{}/json".format(red_pypi)) as r: async with session.get("{}/json".format(red_pypi)) as r:
data = await r.json() data = await r.json()
outdated = StrictVersion(data["info"]["version"]) > StrictVersion(__version__) outdated = VersionInfo.from_str(data["info"]["version"]) > red_version_info
about = ( about = (
"This is an instance of [Red, an open source Discord bot]({}) " "This is an instance of [Red, an open source Discord bot]({}) "
"created by [Twentysix]({}) and [improved by many]({}).\n\n" "created by [Twentysix]({}) and [improved by many]({}).\n\n"

View File

@ -9,7 +9,7 @@ _conn = None
def _initialize(**kwargs): def _initialize(**kwargs):
kwargs.get("URI", "mongodb") uri = kwargs.get("URI", "mongodb")
host = kwargs["HOST"] host = kwargs["HOST"]
port = kwargs["PORT"] port = kwargs["PORT"]
admin_user = kwargs["USERNAME"] admin_user = kwargs["USERNAME"]

View File

@ -1,10 +1,10 @@
import contextlib
import sys import sys
import codecs import codecs
import datetime import datetime
import logging import logging
import traceback import traceback
from datetime import timedelta from datetime import timedelta
from distutils.version import StrictVersion
from typing import List from typing import List
import aiohttp import aiohttp
@ -13,7 +13,7 @@ import pkg_resources
from colorama import Fore, Style, init from colorama import Fore, Style, init
from pkg_resources import DistributionNotFound from pkg_resources import DistributionNotFound
from . import __version__, commands from . import __version__ as red_version, version_info as red_version_info, VersionInfo, commands
from .data_manager import storage_type from .data_manager import storage_type
from .utils.chat_formatting import inline, bordered, humanize_list from .utils.chat_formatting import inline, bordered, humanize_list
from .utils import fuzzy_command_search, format_fuzzy_results from .utils import fuzzy_command_search, format_fuzzy_results
@ -105,7 +105,6 @@ def init_events(bot, cli_flags):
prefixes = cli_flags.prefix or (await bot.db.prefix()) prefixes = cli_flags.prefix or (await bot.db.prefix())
lang = await bot.db.locale() lang = await bot.db.locale()
red_version = __version__
red_pkg = pkg_resources.get_distribution("Red-DiscordBot") red_pkg = pkg_resources.get_distribution("Red-DiscordBot")
dpy_version = discord.__version__ dpy_version = discord.__version__
@ -125,24 +124,22 @@ def init_events(bot, cli_flags):
INFO.append("{} cogs with {} commands".format(len(bot.cogs), len(bot.commands))) INFO.append("{} cogs with {} commands".format(len(bot.cogs), len(bot.commands)))
try: with contextlib.suppress(aiohttp.ClientError, discord.HTTPException):
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r: async with session.get("https://pypi.python.org/pypi/red-discordbot/json") as r:
data = await r.json() data = await r.json()
if StrictVersion(data["info"]["version"]) > StrictVersion(red_version): if VersionInfo.from_str(data["info"]["version"]) > red_version_info:
INFO.append( INFO.append(
"Outdated version! {} is available " "Outdated version! {} is available "
"but you're using {}".format(data["info"]["version"], red_version) "but you're using {}".format(data["info"]["version"], red_version)
) )
owner = discord.utils.get(bot.get_all_members(), id=bot.owner_id) owner = await bot.get_user_info(bot.owner_id)
await owner.send( await owner.send(
"Your Red instance is out of date! {} is the current " "Your Red instance is out of date! {} is the current "
"version, however you are using {}!".format( "version, however you are using {}!".format(
data["info"]["version"], red_version data["info"]["version"], red_version
) )
) )
except:
pass
INFO2 = [] INFO2 = []
sentry = await bot.db.enable_sentry() sentry = await bot.db.enable_sentry()

View File

@ -8,18 +8,14 @@ import asyncio
import aiohttp import aiohttp
import pkg_resources import pkg_resources
from pathlib import Path
from distutils.version import StrictVersion
from redbot.setup import ( from redbot.setup import (
basic_setup, basic_setup,
load_existing_config, load_existing_config,
remove_instance, remove_instance,
remove_instance_interaction, remove_instance_interaction,
create_backup, create_backup,
save_config,
) )
from redbot.core import __version__ from redbot.core import __version__, version_info as red_version_info, VersionInfo
from redbot.core.utils import safe_delete
from redbot.core.cli import confirm from redbot.core.cli import confirm
if sys.platform == "linux": if sys.platform == "linux":
@ -390,7 +386,7 @@ async def is_outdated():
async with session.get("{}/json".format(red_pypi)) as r: async with session.get("{}/json".format(red_pypi)) as r:
data = await r.json() data = await r.json()
new_version = data["info"]["version"] new_version = data["info"]["version"]
return StrictVersion(new_version) > StrictVersion(__version__), new_version return VersionInfo.from_str(new_version) > red_version_info, new_version
def main_menu(): def main_menu():

View File

@ -1,6 +1,36 @@
from redbot import core from redbot import core
from redbot.core import VersionInfo
def test_version_working(): def test_version_working():
assert hasattr(core, "__version__") assert hasattr(core, "__version__")
assert core.__version__[0] == "3" assert core.__version__[0] == "3"
# When adding more of these, ensure they are added in ascending order of precedence
version_tests = (
"3.0.0a32.post10.dev12",
"3.0.0rc1.dev1",
"3.0.0rc1",
"3.0.0",
"3.0.1",
"3.0.1.post1.dev1",
"3.0.1.post1",
"2018.10.6b21",
)
def test_version_info_str_parsing():
for version_str in version_tests:
assert version_str == str(VersionInfo.from_str(version_str))
def test_version_info_lt():
for next_idx, cur in enumerate(version_tests[:-1], start=1):
cur_test = VersionInfo.from_str(cur)
next_test = VersionInfo.from_str(version_tests[next_idx])
assert cur_test < next_test
def test_version_info_gt():
assert VersionInfo.from_str(version_tests[1]) > VersionInfo.from_str(version_tests[0])