[Core] Add deprecation warnings about removal of shared libraries. (#3106)

* feat: add deprecation warning when importing shared libs

* enhance(downloader): add shared libs deprecation warns

* enhance: add deprecation warning when (re)loading cogs

* docs(downloader): add deprecation note about shared libs

* chore(changelog): add towncrier entries

* style: split long tuple unpacks in multiple lines

* fix: argument to `humanize_list` has to be a sequence
This commit is contained in:
jack1142 2019-12-20 08:06:53 +01:00 committed by Michael H
parent 9d027747d1
commit b457f8d1c1
10 changed files with 149 additions and 12 deletions

View File

@ -0,0 +1 @@
Add deprecation note about shared libraries in Downloader Framework docs.

View File

@ -0,0 +1 @@
Send deprecation warning when using `[p]load` and `[p]reload` commands if the repos loaded cogs are from have shared libraries.

View File

@ -0,0 +1 @@
Print deprecation loading when some package tries importing from `cog_shared.*`.

View File

@ -0,0 +1 @@
Shared libraries are marked for removal in Red 3.3.

View File

@ -0,0 +1 @@
Send deprecation warning when using install and update commands if the repos installed/updated cogs are from have shared libraries.

View File

@ -55,6 +55,9 @@ Keys specific to the cog info.json (case sensitive)
- ``type`` (string) - Optional, defaults to ``COG``. Must be either ``COG`` or - ``type`` (string) - Optional, defaults to ``COG``. Must be either ``COG`` or
``SHARED_LIBRARY``. If ``SHARED_LIBRARY`` then ``hidden`` will be ``True``. ``SHARED_LIBRARY``. If ``SHARED_LIBRARY`` then ``hidden`` will be ``True``.
.. warning::
Shared libraries are deprecated since version 3.2 and are marked for removal in version 3.3.
API Reference API Reference
************* *************

View File

@ -33,6 +33,7 @@ from redbot.core.core_commands import Core, license_info_command
from redbot.setup import get_data_dir, get_name, save_config from redbot.setup import get_data_dir, get_name, save_config
from redbot.core.dev_commands import Dev from redbot.core.dev_commands import Dev
from redbot.core import __version__, modlog, bank, data_manager, drivers from redbot.core import __version__, modlog, bank, data_manager, drivers
from redbot.core._sharedlibdeprecation import SharedLibImportWarner
from signal import SIGTERM from signal import SIGTERM
@ -322,6 +323,7 @@ def main():
LIB_PATH.mkdir(parents=True, exist_ok=True) LIB_PATH.mkdir(parents=True, exist_ok=True)
if str(LIB_PATH) not in sys.path: if str(LIB_PATH) not in sys.path:
sys.path.append(str(LIB_PATH)) sys.path.append(str(LIB_PATH))
sys.meta_path.insert(0, SharedLibImportWarner())
red.add_cog(Core(red)) red.add_cog(Core(red))
red.add_cog(CogManagerUI()) red.add_cog(CogManagerUI())

View File

@ -27,6 +27,13 @@ from .repo_manager import RepoManager, Repo
_ = Translator("Downloader", __file__) _ = Translator("Downloader", __file__)
DEPRECATION_NOTICE = _(
"\n**WARNING:** The following repos are using shared libraries"
" which are marked for removal in Red 3.3: {repo_list}.\n"
" You should inform maintainers of these repos about this message."
)
@cog_i18n(_) @cog_i18n(_)
class Downloader(commands.Cog): class Downloader(commands.Cog):
def __init__(self, bot: Red): def __init__(self, bot: Red):
@ -192,6 +199,16 @@ class Downloader(commands.Cog):
await self.conf.installed_cogs.set(installed_cogs) await self.conf.installed_cogs.set(installed_cogs)
await self.conf.installed_libraries.set(installed_libraries) await self.conf.installed_libraries.set(installed_libraries)
async def _shared_lib_load_check(self, cog_name: str) -> Optional[Repo]:
# remove in Red 3.3
is_installed, cog = await self.is_installed(cog_name)
# it's not gonna be None when `is_installed` is True
# if we'll use typing_extensions in future, `Literal` can solve this
cog = cast(InstalledModule, cog)
if is_installed and cog.repo is not None and cog.repo.available_libraries:
return cog.repo
return None
async def _available_updates( async def _available_updates(
self, cogs: Iterable[InstalledModule] self, cogs: Iterable[InstalledModule]
) -> Tuple[Tuple[Installable, ...], Tuple[Installable, ...]]: ) -> Tuple[Tuple[Installable, ...], Tuple[Installable, ...]]:
@ -584,6 +601,9 @@ class Downloader(commands.Cog):
installed_cogs, failed_cogs = await self._install_cogs(cogs) installed_cogs, failed_cogs = await self._install_cogs(cogs)
deprecation_notice = ""
if repo.available_libraries:
deprecation_notice = DEPRECATION_NOTICE.format(repo_list=inline(repo.name))
installed_libs, failed_libs = await repo.install_libraries( installed_libs, failed_libs = await repo.install_libraries(
target_dir=self.SHAREDLIB_PATH, req_target_dir=self.LIB_PATH target_dir=self.SHAREDLIB_PATH, req_target_dir=self.LIB_PATH
) )
@ -622,7 +642,7 @@ class Downloader(commands.Cog):
+ message + message
) )
# "---" added to separate cog install messages from Downloader's message # "---" added to separate cog install messages from Downloader's message
await ctx.send(f"{message}\n---") await ctx.send(f"{message}{deprecation_notice}\n---")
for cog in installed_cogs: for cog in installed_cogs:
if cog.install_msg: if cog.install_msg:
await ctx.send(cog.install_msg.replace("[p]", ctx.prefix)) await ctx.send(cog.install_msg.replace("[p]", ctx.prefix))
@ -874,6 +894,14 @@ class Downloader(commands.Cog):
if failed_repos: if failed_repos:
message += "\n" + self.format_failed_repos(failed_repos) message += "\n" + self.format_failed_repos(failed_repos)
repos_with_libs = {
inline(module.repo.name)
for module in cogs_to_update + libs_to_update
if module.repo.available_libraries
}
if repos_with_libs:
message += DEPRECATION_NOTICE.format(repo_list=humanize_list(list(repos_with_libs)))
await ctx.send(message) await ctx.send(message)
if updates_available and updated_cognames: if updates_available and updated_cognames:

View File

@ -0,0 +1,29 @@
from importlib.abc import MetaPathFinder
import warnings
class SharedLibDeprecationWarning(DeprecationWarning):
pass
warnings.simplefilter("always", SharedLibDeprecationWarning)
class SharedLibImportWarner(MetaPathFinder):
"""
Deprecation warner for shared libraries. This class sits on `sys.meta_path`
and prints warning if imported module is a shared library
"""
def find_spec(self, fullname, path, target=None) -> None:
"""This is only supposed to print warnings, it won't ever return module spec."""
parts = fullname.split(".")
if parts[0] != "cog_shared" or len(parts) != 2:
return None
msg = (
"One of cogs uses shared libraries which are"
" deprecated and scheduled for removal in Red 3.3.\n"
"You should inform author of the cog about this message."
)
warnings.warn(msg, SharedLibDeprecationWarning, stacklevel=2)
return None

View File

@ -14,7 +14,7 @@ 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 typing import TYPE_CHECKING, Union, Tuple, List, Optional, Iterable, Sequence, Dict from typing import TYPE_CHECKING, Union, Tuple, List, Optional, Iterable, Sequence, Dict, Set
import aiohttp import aiohttp
import discord import discord
@ -70,7 +70,7 @@ class CoreLogic:
async def _load( async def _load(
self, cog_names: Iterable[str] self, cog_names: Iterable[str]
) -> Tuple[List[str], List[str], List[str], List[str], List[Tuple[str, str]]]: ) -> Tuple[List[str], List[str], List[str], List[str], List[Tuple[str, str]], Set[str]]:
""" """
Loads cogs by name. Loads cogs by name.
Parameters Parameters
@ -87,6 +87,7 @@ class CoreLogic:
notfound_packages = [] notfound_packages = []
alreadyloaded_packages = [] alreadyloaded_packages = []
failed_with_reason_packages = [] failed_with_reason_packages = []
repos_with_shared_libs = set()
bot = self.bot bot = self.bot
@ -125,6 +126,20 @@ class CoreLogic:
else: else:
await bot.add_loaded_package(name) await bot.add_loaded_package(name)
loaded_packages.append(name) loaded_packages.append(name)
# remove in Red 3.3
downloader = bot.get_cog("Downloader")
if downloader is None:
continue
try:
maybe_repo = await downloader._shared_lib_load_check(name)
except Exception:
log.exception(
"Shared library check failed,"
" if you're not using modified Downloader, report this issue."
)
maybe_repo = None
if maybe_repo is not None:
repos_with_shared_libs.add(maybe_repo.name)
return ( return (
loaded_packages, loaded_packages,
@ -132,6 +147,7 @@ class CoreLogic:
notfound_packages, notfound_packages,
alreadyloaded_packages, alreadyloaded_packages,
failed_with_reason_packages, failed_with_reason_packages,
repos_with_shared_libs,
) )
@staticmethod @staticmethod
@ -186,14 +202,26 @@ class CoreLogic:
async def _reload( async def _reload(
self, cog_names: Sequence[str] self, cog_names: Sequence[str]
) -> Tuple[List[str], List[str], List[str], List[str], List[Tuple[str, str]]]: ) -> Tuple[List[str], List[str], List[str], List[str], List[Tuple[str, str]], Set[str]]:
await self._unload(cog_names) await self._unload(cog_names)
loaded, load_failed, not_found, already_loaded, load_failed_with_reason = await self._load( (
cog_names loaded,
) load_failed,
not_found,
already_loaded,
load_failed_with_reason,
repos_with_shared_libs,
) = await self._load(cog_names)
return loaded, load_failed, not_found, already_loaded, load_failed_with_reason return (
loaded,
load_failed,
not_found,
already_loaded,
load_failed_with_reason,
repos_with_shared_libs,
)
async def _name(self, name: Optional[str] = None) -> str: async def _name(self, name: Optional[str] = None) -> str:
""" """
@ -580,7 +608,14 @@ class Core(commands.Cog, CoreLogic):
return await ctx.send_help() return await ctx.send_help()
cogs = tuple(map(lambda cog: cog.rstrip(","), cogs)) cogs = tuple(map(lambda cog: cog.rstrip(","), cogs))
async with ctx.typing(): async with ctx.typing():
loaded, failed, not_found, already_loaded, failed_with_reason = await self._load(cogs) (
loaded,
failed,
not_found,
already_loaded,
failed_with_reason,
repos_with_shared_libs,
) = await self._load(cogs)
output = [] output = []
@ -636,6 +671,21 @@ class Core(commands.Cog, CoreLogic):
).format(reasons=reasons) ).format(reasons=reasons)
output.append(formed) output.append(formed)
if repos_with_shared_libs:
if len(repos_with_shared_libs) == 1:
formed = _(
"**WARNING**: The following repo is using shared libs"
" which are marked for removal in Red 3.3: {repo}.\n"
"You should inform maintainer of the repo about this message."
).format(repo=inline(repos_with_shared_libs.pop()))
else:
formed = _(
"**WARNING**: The following repos are using shared libs"
" which are marked for removal in Red 3.3: {repos}.\n"
"You should inform maintainers of these repos about this message."
).format(repos=humanize_list([inline(repo) for repo in repos_with_shared_libs]))
output.append(formed)
if output: if output:
total_message = "\n\n".join(output) total_message = "\n\n".join(output)
for page in pagify(total_message): for page in pagify(total_message):
@ -687,9 +737,14 @@ class Core(commands.Cog, CoreLogic):
return await ctx.send_help() return await ctx.send_help()
cogs = tuple(map(lambda cog: cog.rstrip(","), cogs)) cogs = tuple(map(lambda cog: cog.rstrip(","), cogs))
async with ctx.typing(): async with ctx.typing():
loaded, failed, not_found, already_loaded, failed_with_reason = await self._reload( (
cogs loaded,
) failed,
not_found,
already_loaded,
failed_with_reason,
repos_with_shared_libs,
) = await self._reload(cogs)
output = [] output = []
@ -734,6 +789,21 @@ class Core(commands.Cog, CoreLogic):
).format(reasons=reasons) ).format(reasons=reasons)
output.append(formed) output.append(formed)
if repos_with_shared_libs:
if len(repos_with_shared_libs) == 1:
formed = _(
"**WARNING**: The following repo is using shared libs"
" which are marked for removal in Red 3.3: {repo}.\n"
"You should inform maintainers of these repos about this message."
).format(repo=inline(repos_with_shared_libs.pop()))
else:
formed = _(
"**WARNING**: The following repos are using shared libs"
" which are marked for removal in Red 3.3: {repos}.\n"
"You should inform maintainers of these repos about this message."
).format(repos=humanize_list([inline(repo) for repo in repos_with_shared_libs]))
output.append(formed)
if output: if output:
total_message = "\n\n".join(output) total_message = "\n\n".join(output)
for page in pagify(total_message): for page in pagify(total_message):