mirror of
https://github.com/Cog-Creators/Red-DiscordBot.git
synced 2026-04-21 18:49:17 -04:00
Rip out Downloader's non-UI functionality into private core API (#6706)
This commit is contained in:
11
.github/labeler.yml
vendored
11
.github/labeler.yml
vendored
@@ -50,10 +50,6 @@
|
|||||||
- redbot/cogs/downloader/*
|
- redbot/cogs/downloader/*
|
||||||
# Docs
|
# Docs
|
||||||
- docs/cog_guides/downloader.rst
|
- docs/cog_guides/downloader.rst
|
||||||
# Tests
|
|
||||||
- redbot/pytest/downloader.py
|
|
||||||
- redbot/pytest/downloader_testrepo.*
|
|
||||||
- tests/cogs/downloader/**/*
|
|
||||||
"Category: Cogs - Economy":
|
"Category: Cogs - Economy":
|
||||||
# Source
|
# Source
|
||||||
- redbot/cogs/economy/*
|
- redbot/cogs/economy/*
|
||||||
@@ -212,6 +208,13 @@
|
|||||||
- redbot/core/_cli.py
|
- redbot/core/_cli.py
|
||||||
- redbot/core/_debuginfo.py
|
- redbot/core/_debuginfo.py
|
||||||
- redbot/setup.py
|
- redbot/setup.py
|
||||||
|
"Category: Core - Downloader":
|
||||||
|
# Source
|
||||||
|
- redbot/core/_downloader/**/*
|
||||||
|
# Tests
|
||||||
|
- redbot/pytest/downloader.py
|
||||||
|
- redbot/pytest/downloader_testrepo.*
|
||||||
|
- tests/core/_downloader/**/*
|
||||||
"Category: Core - Help":
|
"Category: Core - Help":
|
||||||
- redbot/core/commands/help.py
|
- redbot/core/commands/help.py
|
||||||
"Category: Core - i18n":
|
"Category: Core - i18n":
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ from redbot import __version__
|
|||||||
from redbot.core.bot import Red, ExitCodes, _NoOwnerSet
|
from redbot.core.bot import Red, ExitCodes, _NoOwnerSet
|
||||||
from redbot.core._cli import interactive_config, confirm, parse_cli_flags
|
from redbot.core._cli import interactive_config, confirm, parse_cli_flags
|
||||||
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 import data_manager, _drivers
|
from redbot.core import data_manager, _drivers, _downloader
|
||||||
from redbot.core._debuginfo import DebugInfo
|
from redbot.core._debuginfo import DebugInfo
|
||||||
from redbot.core._sharedlibdeprecation import SharedLibImportWarner
|
from redbot.core._sharedlibdeprecation import SharedLibImportWarner
|
||||||
|
|
||||||
@@ -324,12 +324,13 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None:
|
|||||||
log.debug("Data Path: %s", data_manager._base_data_path())
|
log.debug("Data Path: %s", data_manager._base_data_path())
|
||||||
log.debug("Storage Type: %s", data_manager.storage_type())
|
log.debug("Storage Type: %s", data_manager.storage_type())
|
||||||
|
|
||||||
|
await _downloader._init(red)
|
||||||
|
|
||||||
# lib folder has to be in sys.path before trying to load any 3rd-party cog (GH-3061)
|
# lib folder has to be in sys.path before trying to load any 3rd-party cog (GH-3061)
|
||||||
# We might want to change handling of requirements in Downloader at later date
|
# We might want to change handling of requirements in Downloader at later date
|
||||||
LIB_PATH = data_manager.cog_data_path(raw_name="Downloader") / "lib"
|
lib_path = str(_downloader.LIB_PATH)
|
||||||
LIB_PATH.mkdir(parents=True, exist_ok=True)
|
if lib_path not in sys.path:
|
||||||
if str(LIB_PATH) not in sys.path:
|
sys.path.append(lib_path)
|
||||||
sys.path.append(str(LIB_PATH))
|
|
||||||
|
|
||||||
# "It's important to note that the global `working_set` object is initialized from
|
# "It's important to note that the global `working_set` object is initialized from
|
||||||
# `sys.path` when `pkg_resources` is first imported, but is only updated if you do
|
# `sys.path` when `pkg_resources` is first imported, but is only updated if you do
|
||||||
@@ -339,7 +340,7 @@ async def run_bot(red: Red, cli_flags: Namespace) -> None:
|
|||||||
# Source: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#workingset-objects
|
# Source: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#workingset-objects
|
||||||
pkg_resources = sys.modules.get("pkg_resources")
|
pkg_resources = sys.modules.get("pkg_resources")
|
||||||
if pkg_resources is not None:
|
if pkg_resources is not None:
|
||||||
pkg_resources.working_set.add_entry(str(LIB_PATH))
|
pkg_resources.working_set.add_entry(lib_path)
|
||||||
sys.meta_path.insert(0, SharedLibImportWarner())
|
sys.meta_path.insert(0, SharedLibImportWarner())
|
||||||
|
|
||||||
if cli_flags.token:
|
if cli_flags.token:
|
||||||
|
|||||||
@@ -6,4 +6,3 @@ from .downloader import Downloader
|
|||||||
async def setup(bot: Red) -> None:
|
async def setup(bot: Red) -> None:
|
||||||
cog = Downloader(bot)
|
cog = Downloader(bot)
|
||||||
await bot.add_cog(cog)
|
await bot.add_cog(cog)
|
||||||
cog.create_init_task()
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import discord
|
import discord
|
||||||
from redbot.core import commands
|
from redbot.core import _downloader, commands
|
||||||
from redbot.core.i18n import Translator
|
from redbot.core.i18n import Translator
|
||||||
from .installable import InstalledModule
|
from redbot.core._downloader.installable import InstalledModule
|
||||||
|
from redbot.core._downloader.repo_manager import Repo as _Repo
|
||||||
|
|
||||||
_ = Translator("Koala", __file__)
|
_ = Translator("Koala", __file__)
|
||||||
|
|
||||||
@@ -9,14 +10,21 @@ _ = Translator("Koala", __file__)
|
|||||||
class InstalledCog(InstalledModule):
|
class InstalledCog(InstalledModule):
|
||||||
@classmethod
|
@classmethod
|
||||||
async def convert(cls, ctx: commands.Context, arg: str) -> InstalledModule:
|
async def convert(cls, ctx: commands.Context, arg: str) -> InstalledModule:
|
||||||
downloader = ctx.bot.get_cog("Downloader")
|
cog = discord.utils.get(await _downloader.installed_cogs(), name=arg)
|
||||||
if downloader is None:
|
|
||||||
raise commands.CommandError(_("No Downloader cog found."))
|
|
||||||
|
|
||||||
cog = discord.utils.get(await downloader.installed_cogs(), name=arg)
|
|
||||||
if cog is None:
|
if cog is None:
|
||||||
raise commands.BadArgument(
|
raise commands.BadArgument(
|
||||||
_("Cog `{cog_name}` is not installed.").format(cog_name=arg)
|
_("Cog `{cog_name}` is not installed.").format(cog_name=arg)
|
||||||
)
|
)
|
||||||
|
|
||||||
return cog
|
return cog
|
||||||
|
|
||||||
|
|
||||||
|
class Repo(_Repo):
|
||||||
|
@classmethod
|
||||||
|
async def convert(cls, ctx: commands.Context, argument: str) -> _Repo:
|
||||||
|
poss_repo = _downloader._repo_manager.get_repo(argument)
|
||||||
|
if poss_repo is None:
|
||||||
|
raise commands.BadArgument(
|
||||||
|
_('Repo by the name "{repo_name}" does not exist.').format(repo_name=argument)
|
||||||
|
)
|
||||||
|
return poss_repo
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
865
redbot/core/_downloader/__init__.py
Normal file
865
redbot/core/_downloader/__init__.py
Normal file
@@ -0,0 +1,865 @@
|
|||||||
|
# TODO list:
|
||||||
|
# - design ergonomic APIs instead of whatever you want to call what we have now
|
||||||
|
# - try to be consistent about requiring Installable vs cog name
|
||||||
|
# between cog install and other functionality
|
||||||
|
# - use immutable objects more
|
||||||
|
# - change Installable's equality to include its commit
|
||||||
|
# (note: we currently heavily rely on this *not* being the case)
|
||||||
|
# - add asyncio.Lock appropriately for things that Downloader does
|
||||||
|
# - avoid doing some of the work on RepoManager initialization to speedup bot startup
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import dataclasses
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import (
|
||||||
|
Dict,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Literal,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
TYPE_CHECKING,
|
||||||
|
)
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from redbot.core import commands, Config, version_info as red_version_info
|
||||||
|
from redbot.core._cog_manager import CogManager
|
||||||
|
from redbot.core.data_manager import cog_data_path
|
||||||
|
|
||||||
|
from . import errors
|
||||||
|
from .log import log
|
||||||
|
from .installable import InstallableType, Installable, InstalledModule
|
||||||
|
from .repo_manager import RepoManager, Repo
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from redbot.core.bot import Red
|
||||||
|
|
||||||
|
|
||||||
|
_SCHEMA_VERSION = 1
|
||||||
|
_config: Config
|
||||||
|
_bot_ref: Optional[Red]
|
||||||
|
_cog_mgr: CogManager
|
||||||
|
_repo_manager: RepoManager
|
||||||
|
|
||||||
|
LIB_PATH: Path
|
||||||
|
SHAREDLIB_PATH: Path
|
||||||
|
_SHAREDLIB_INIT: Path
|
||||||
|
|
||||||
|
|
||||||
|
async def _init(bot: Red) -> None:
|
||||||
|
global _bot_ref
|
||||||
|
_bot_ref = bot
|
||||||
|
|
||||||
|
await _init_without_bot(_bot_ref._cog_mgr)
|
||||||
|
|
||||||
|
|
||||||
|
async def _init_without_bot(cog_manager: CogManager) -> None:
|
||||||
|
global _cog_mgr
|
||||||
|
_cog_mgr = cog_manager
|
||||||
|
|
||||||
|
start = time.perf_counter()
|
||||||
|
|
||||||
|
global _config
|
||||||
|
_config = Config.get_conf(None, 998240343, cog_name="Downloader", force_registration=True)
|
||||||
|
_config.register_global(schema_version=0, installed_cogs={}, installed_libraries={})
|
||||||
|
await _migrate_config()
|
||||||
|
|
||||||
|
global LIB_PATH, SHAREDLIB_PATH, _SHAREDLIB_INIT
|
||||||
|
LIB_PATH = cog_data_path(raw_name="Downloader") / "lib"
|
||||||
|
SHAREDLIB_PATH = LIB_PATH / "cog_shared"
|
||||||
|
_SHAREDLIB_INIT = SHAREDLIB_PATH / "__init__.py"
|
||||||
|
_create_lib_folder()
|
||||||
|
|
||||||
|
global _repo_manager
|
||||||
|
_repo_manager = RepoManager()
|
||||||
|
await _repo_manager.initialize()
|
||||||
|
|
||||||
|
stop = time.perf_counter()
|
||||||
|
|
||||||
|
log.debug("Finished initialization in %.2fs", stop - start)
|
||||||
|
|
||||||
|
|
||||||
|
async def _migrate_config() -> None:
|
||||||
|
schema_version = await _config.schema_version()
|
||||||
|
|
||||||
|
if schema_version == _SCHEMA_VERSION:
|
||||||
|
return
|
||||||
|
|
||||||
|
if schema_version == 0:
|
||||||
|
await _schema_0_to_1()
|
||||||
|
schema_version += 1
|
||||||
|
await _config.schema_version.set(schema_version)
|
||||||
|
|
||||||
|
|
||||||
|
async def _schema_0_to_1():
|
||||||
|
"""
|
||||||
|
This contains migration to allow saving state
|
||||||
|
of both installed cogs and shared libraries.
|
||||||
|
"""
|
||||||
|
old_conf = await _config.get_raw("installed", default=[])
|
||||||
|
if not old_conf:
|
||||||
|
return
|
||||||
|
async with _config.installed_cogs() as new_cog_conf:
|
||||||
|
for cog_json in old_conf:
|
||||||
|
repo_name = cog_json["repo_name"]
|
||||||
|
module_name = cog_json["cog_name"]
|
||||||
|
if repo_name not in new_cog_conf:
|
||||||
|
new_cog_conf[repo_name] = {}
|
||||||
|
new_cog_conf[repo_name][module_name] = {
|
||||||
|
"repo_name": repo_name,
|
||||||
|
"module_name": module_name,
|
||||||
|
"commit": "",
|
||||||
|
"pinned": False,
|
||||||
|
}
|
||||||
|
await _config.clear_raw("installed")
|
||||||
|
# no reliable way to get installed libraries (i.a. missing repo name)
|
||||||
|
# but it only helps `[p]cog update` run faster so it's not an issue
|
||||||
|
|
||||||
|
|
||||||
|
def _create_lib_folder(*, remove_first: bool = False) -> None:
|
||||||
|
if remove_first:
|
||||||
|
shutil.rmtree(str(LIB_PATH))
|
||||||
|
SHAREDLIB_PATH.mkdir(parents=True, exist_ok=True)
|
||||||
|
if not _SHAREDLIB_INIT.exists():
|
||||||
|
with _SHAREDLIB_INIT.open(mode="w", encoding="utf-8") as _:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def installed_cogs() -> Tuple[InstalledModule, ...]:
|
||||||
|
"""Get info on installed cogs.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
`tuple` of `InstalledModule`
|
||||||
|
All installed cogs.
|
||||||
|
|
||||||
|
"""
|
||||||
|
installed = await _config.installed_cogs()
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
return tuple(
|
||||||
|
InstalledModule.from_json(cog_json, _repo_manager)
|
||||||
|
for repo_json in installed.values()
|
||||||
|
for cog_json in repo_json.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def installed_libraries() -> Tuple[InstalledModule, ...]:
|
||||||
|
"""Get info on installed shared libraries.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
`tuple` of `InstalledModule`
|
||||||
|
All installed shared libraries.
|
||||||
|
|
||||||
|
"""
|
||||||
|
installed = await _config.installed_libraries()
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
return tuple(
|
||||||
|
InstalledModule.from_json(lib_json, _repo_manager)
|
||||||
|
for repo_json in installed.values()
|
||||||
|
for lib_json in repo_json.values()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def installed_modules() -> Tuple[InstalledModule, ...]:
|
||||||
|
"""Get info on installed cogs and shared libraries.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
`tuple` of `InstalledModule`
|
||||||
|
All installed cogs and shared libraries.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return await installed_cogs() + await installed_libraries()
|
||||||
|
|
||||||
|
|
||||||
|
async def _save_to_installed(modules: Iterable[InstalledModule]) -> None:
|
||||||
|
"""Mark modules as installed or updates their json in Config.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
modules : `list` of `InstalledModule`
|
||||||
|
The modules to check off.
|
||||||
|
|
||||||
|
"""
|
||||||
|
async with _config.all() as global_data:
|
||||||
|
installed_cogs = global_data["installed_cogs"]
|
||||||
|
installed_libraries = global_data["installed_libraries"]
|
||||||
|
for module in modules:
|
||||||
|
if module.type is InstallableType.COG:
|
||||||
|
installed = installed_cogs
|
||||||
|
elif module.type is InstallableType.SHARED_LIBRARY:
|
||||||
|
installed = installed_libraries
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
module_json = module.to_json()
|
||||||
|
repo_json = installed.setdefault(module.repo_name, {})
|
||||||
|
repo_json[module.name] = module_json
|
||||||
|
|
||||||
|
|
||||||
|
async def _remove_from_installed(modules: Iterable[InstalledModule]) -> None:
|
||||||
|
"""Remove modules from the saved list
|
||||||
|
of installed modules (corresponding to type of module).
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
modules : `list` of `InstalledModule`
|
||||||
|
The modules to remove.
|
||||||
|
|
||||||
|
"""
|
||||||
|
async with _config.all() as global_data:
|
||||||
|
installed_cogs = global_data["installed_cogs"]
|
||||||
|
installed_libraries = global_data["installed_libraries"]
|
||||||
|
for module in modules:
|
||||||
|
if module.type is InstallableType.COG:
|
||||||
|
installed = installed_cogs
|
||||||
|
elif module.type is InstallableType.SHARED_LIBRARY:
|
||||||
|
installed = installed_libraries
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
with contextlib.suppress(KeyError):
|
||||||
|
installed[module._json_repo_name].pop(module.name)
|
||||||
|
|
||||||
|
|
||||||
|
async def _shared_lib_load_check(cog_name: str) -> Optional[Repo]:
|
||||||
|
_is_installed, cog = await is_installed(cog_name)
|
||||||
|
if _is_installed and cog.repo is not None and cog.repo.available_libraries:
|
||||||
|
return cog.repo
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def is_installed(
|
||||||
|
cog_name: str,
|
||||||
|
) -> Union[Tuple[Literal[True], InstalledModule], Tuple[Literal[False], None]]:
|
||||||
|
"""Check to see if a cog has been installed through Downloader.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cog_name : str
|
||||||
|
The name of the cog to check for.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
`tuple` of (`bool`, `InstalledModule`)
|
||||||
|
:code:`(True, InstalledModule)` if the cog is installed, else
|
||||||
|
:code:`(False, None)`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for installed_cog in await installed_cogs():
|
||||||
|
if installed_cog.name == cog_name:
|
||||||
|
return True, installed_cog
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
async def _available_updates(
|
||||||
|
cogs: Iterable[InstalledModule],
|
||||||
|
) -> Tuple[Tuple[Installable, ...], Tuple[Installable, ...]]:
|
||||||
|
"""
|
||||||
|
Get cogs and libraries which can be updated.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cogs : `list` of `InstalledModule`
|
||||||
|
List of cogs, which should be checked against the updates.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
2-tuple of cogs and libraries which can be updated.
|
||||||
|
|
||||||
|
"""
|
||||||
|
repos = {cog.repo for cog in cogs if cog.repo is not None}
|
||||||
|
_installed_libraries = await installed_libraries()
|
||||||
|
|
||||||
|
modules: Set[InstalledModule] = set()
|
||||||
|
cogs_to_update: Set[Installable] = set()
|
||||||
|
libraries_to_update: Set[Installable] = set()
|
||||||
|
# split libraries and cogs into 2 categories:
|
||||||
|
# 1. `cogs_to_update`, `libraries_to_update` - module needs update, skip diffs
|
||||||
|
# 2. `modules` - module MAY need update, check diffs
|
||||||
|
for repo in repos:
|
||||||
|
for lib in repo.available_libraries:
|
||||||
|
try:
|
||||||
|
index = _installed_libraries.index(lib)
|
||||||
|
except ValueError:
|
||||||
|
libraries_to_update.add(lib)
|
||||||
|
else:
|
||||||
|
modules.add(_installed_libraries[index])
|
||||||
|
for cog in cogs:
|
||||||
|
if cog.repo is None:
|
||||||
|
# cog had its repo removed, can't check for updates
|
||||||
|
continue
|
||||||
|
if cog.commit:
|
||||||
|
modules.add(cog)
|
||||||
|
continue
|
||||||
|
# marking cog for update if there's no commit data saved (back-compat, see GH-2571)
|
||||||
|
last_cog_occurrence = await cog.repo.get_last_module_occurrence(cog.name)
|
||||||
|
if last_cog_occurrence is not None and not last_cog_occurrence.disabled:
|
||||||
|
cogs_to_update.add(last_cog_occurrence)
|
||||||
|
|
||||||
|
# Reduces diff requests to a single dict with no repeats
|
||||||
|
hashes: Dict[Tuple[Repo, str], Set[InstalledModule]] = defaultdict(set)
|
||||||
|
for module in modules:
|
||||||
|
module.repo = cast(Repo, module.repo)
|
||||||
|
if module.repo.commit != module.commit:
|
||||||
|
try:
|
||||||
|
should_add = await module.repo.is_ancestor(module.commit, module.repo.commit)
|
||||||
|
except errors.UnknownRevision:
|
||||||
|
# marking module for update if the saved commit data is invalid
|
||||||
|
last_module_occurrence = await module.repo.get_last_module_occurrence(module.name)
|
||||||
|
if last_module_occurrence is not None and not last_module_occurrence.disabled:
|
||||||
|
if last_module_occurrence.type is InstallableType.COG:
|
||||||
|
cogs_to_update.add(last_module_occurrence)
|
||||||
|
elif last_module_occurrence.type is InstallableType.SHARED_LIBRARY:
|
||||||
|
libraries_to_update.add(last_module_occurrence)
|
||||||
|
else:
|
||||||
|
if should_add:
|
||||||
|
hashes[(module.repo, module.commit)].add(module)
|
||||||
|
|
||||||
|
update_commits = []
|
||||||
|
for (repo, old_hash), modules_to_check in hashes.items():
|
||||||
|
modified = await repo.get_modified_modules(old_hash, repo.commit)
|
||||||
|
for module in modules_to_check:
|
||||||
|
try:
|
||||||
|
index = modified.index(module)
|
||||||
|
except ValueError:
|
||||||
|
# module wasn't modified - we just need to update its commit
|
||||||
|
module.commit = repo.commit
|
||||||
|
update_commits.append(module)
|
||||||
|
else:
|
||||||
|
modified_module = modified[index]
|
||||||
|
if modified_module.type is InstallableType.COG:
|
||||||
|
if not modified_module.disabled:
|
||||||
|
cogs_to_update.add(modified_module)
|
||||||
|
elif modified_module.type is InstallableType.SHARED_LIBRARY:
|
||||||
|
libraries_to_update.add(modified_module)
|
||||||
|
|
||||||
|
await _save_to_installed(update_commits)
|
||||||
|
|
||||||
|
return (tuple(cogs_to_update), tuple(libraries_to_update))
|
||||||
|
|
||||||
|
|
||||||
|
async def _install_cogs(
|
||||||
|
cogs: Iterable[Installable],
|
||||||
|
) -> Tuple[Tuple[InstalledModule, ...], Tuple[Installable, ...]]:
|
||||||
|
"""Installs a list of cogs.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cogs : `list` of `Installable`
|
||||||
|
Cogs to install. ``repo`` property of those objects can't be `None`
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
2-tuple of installed and failed cogs.
|
||||||
|
"""
|
||||||
|
repos: Dict[str, Tuple[Repo, Dict[str, List[Installable]]]] = {}
|
||||||
|
for cog in cogs:
|
||||||
|
try:
|
||||||
|
repo_by_commit = repos[cog.repo_name]
|
||||||
|
except KeyError:
|
||||||
|
cog.repo = cast(Repo, cog.repo) # docstring specifies this already
|
||||||
|
repo_by_commit = repos[cog.repo_name] = (cog.repo, defaultdict(list))
|
||||||
|
cogs_by_commit = repo_by_commit[1]
|
||||||
|
cogs_by_commit[cog.commit].append(cog)
|
||||||
|
installed = []
|
||||||
|
failed = []
|
||||||
|
for repo, cogs_by_commit in repos.values():
|
||||||
|
exit_to_commit = repo.commit
|
||||||
|
for commit, cogs_to_install in cogs_by_commit.items():
|
||||||
|
await repo.checkout(commit)
|
||||||
|
for cog in cogs_to_install:
|
||||||
|
if await cog.copy_to(await _cog_mgr.install_path()):
|
||||||
|
installed.append(InstalledModule.from_installable(cog))
|
||||||
|
else:
|
||||||
|
failed.append(cog)
|
||||||
|
await repo.checkout(exit_to_commit)
|
||||||
|
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
return (tuple(installed), tuple(failed))
|
||||||
|
|
||||||
|
|
||||||
|
async def _reinstall_libraries(
|
||||||
|
libraries: Iterable[Installable],
|
||||||
|
) -> Tuple[Tuple[InstalledModule, ...], Tuple[Installable, ...]]:
|
||||||
|
"""Installs a list of shared libraries, used when updating.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
libraries : `list` of `Installable`
|
||||||
|
Libraries to reinstall. ``repo`` property of those objects can't be `None`
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
2-tuple of installed and failed libraries.
|
||||||
|
"""
|
||||||
|
repos: Dict[str, Tuple[Repo, Dict[str, Set[Installable]]]] = {}
|
||||||
|
for lib in libraries:
|
||||||
|
try:
|
||||||
|
repo_by_commit = repos[lib.repo_name]
|
||||||
|
except KeyError:
|
||||||
|
lib.repo = cast(Repo, lib.repo) # docstring specifies this already
|
||||||
|
repo_by_commit = repos[lib.repo_name] = (lib.repo, defaultdict(set))
|
||||||
|
libs_by_commit = repo_by_commit[1]
|
||||||
|
libs_by_commit[lib.commit].add(lib)
|
||||||
|
|
||||||
|
all_installed: List[InstalledModule] = []
|
||||||
|
all_failed: List[Installable] = []
|
||||||
|
for repo, libs_by_commit in repos.values():
|
||||||
|
exit_to_commit = repo.commit
|
||||||
|
for commit, libs in libs_by_commit.items():
|
||||||
|
await repo.checkout(commit)
|
||||||
|
installed, failed = await repo.install_libraries(
|
||||||
|
target_dir=SHAREDLIB_PATH, req_target_dir=LIB_PATH, libraries=libs
|
||||||
|
)
|
||||||
|
all_installed += installed
|
||||||
|
all_failed += failed
|
||||||
|
await repo.checkout(exit_to_commit)
|
||||||
|
|
||||||
|
# noinspection PyTypeChecker
|
||||||
|
return (tuple(all_installed), tuple(all_failed))
|
||||||
|
|
||||||
|
|
||||||
|
async def _install_requirements(cogs: Iterable[Installable]) -> Tuple[str, ...]:
|
||||||
|
"""
|
||||||
|
Installs requirements for given cogs.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
cogs : `list` of `Installable`
|
||||||
|
Cogs whose requirements should be installed.
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
tuple
|
||||||
|
Tuple of failed requirements.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Reduces requirements to a single list with no repeats
|
||||||
|
requirements = {requirement for cog in cogs for requirement in cog.requirements}
|
||||||
|
repos: List[Tuple[Repo, List[str]]] = [(repo, []) for repo in _repo_manager.repos]
|
||||||
|
|
||||||
|
# This for loop distributes the requirements across all repos
|
||||||
|
# which will allow us to concurrently install requirements
|
||||||
|
for i, req in enumerate(requirements):
|
||||||
|
repo_index = i % len(repos)
|
||||||
|
repos[repo_index][1].append(req)
|
||||||
|
|
||||||
|
has_reqs = list(filter(lambda item: len(item[1]) > 0, repos))
|
||||||
|
|
||||||
|
failed_reqs = []
|
||||||
|
for repo, reqs in has_reqs:
|
||||||
|
for req in reqs:
|
||||||
|
if not await repo.install_raw_requirements([req], LIB_PATH):
|
||||||
|
failed_reqs.append(req)
|
||||||
|
return tuple(failed_reqs)
|
||||||
|
|
||||||
|
|
||||||
|
async def _delete_cog(target: Path) -> None:
|
||||||
|
"""
|
||||||
|
Removes an (installed) cog.
|
||||||
|
:param target: Path pointing to an existing file or directory
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not target.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
if target.is_dir():
|
||||||
|
shutil.rmtree(str(target))
|
||||||
|
elif target.is_file():
|
||||||
|
os.remove(str(target))
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_cogs_to_check(
|
||||||
|
*,
|
||||||
|
repos: Optional[Iterable[Repo]] = None,
|
||||||
|
cogs: Optional[Iterable[InstalledModule]] = None,
|
||||||
|
update_repos: bool = True,
|
||||||
|
) -> Tuple[Set[InstalledModule], List[str]]:
|
||||||
|
failed = []
|
||||||
|
if not (cogs or repos):
|
||||||
|
if update_repos:
|
||||||
|
__, failed = await _repo_manager.update_repos()
|
||||||
|
|
||||||
|
cogs_to_check = {
|
||||||
|
cog
|
||||||
|
for cog in await installed_cogs()
|
||||||
|
if cog.repo is not None and cog.repo.name not in failed
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# this is enough to be sure that `cogs` is not None (based on if above)
|
||||||
|
if not repos:
|
||||||
|
cogs = cast(Iterable[InstalledModule], cogs)
|
||||||
|
repos = {cog.repo for cog in cogs if cog.repo is not None}
|
||||||
|
|
||||||
|
if update_repos:
|
||||||
|
__, failed = await _repo_manager.update_repos(repos)
|
||||||
|
|
||||||
|
if failed:
|
||||||
|
# remove failed repos
|
||||||
|
repos = {repo for repo in repos if repo.name not in failed}
|
||||||
|
|
||||||
|
if cogs:
|
||||||
|
cogs_to_check = {cog for cog in cogs if cog.repo is not None and cog.repo in repos}
|
||||||
|
else:
|
||||||
|
cogs_to_check = {
|
||||||
|
cog for cog in await installed_cogs() if cog.repo is not None and cog.repo in repos
|
||||||
|
}
|
||||||
|
|
||||||
|
return (cogs_to_check, failed)
|
||||||
|
|
||||||
|
|
||||||
|
# functionality extracted from command implementations
|
||||||
|
# TODO: make them into nice APIs instead of what they are now...
|
||||||
|
|
||||||
|
|
||||||
|
async def pip_install(*deps: str) -> bool:
|
||||||
|
repo = Repo("", "", "", "", Path.cwd())
|
||||||
|
return await repo.install_raw_requirements(deps, LIB_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
async def reinstall_requirements() -> tuple[List[str], List[str]]:
|
||||||
|
_create_lib_folder(remove_first=True)
|
||||||
|
_installed_cogs = await installed_cogs()
|
||||||
|
cogs = []
|
||||||
|
repos = set()
|
||||||
|
for cog in _installed_cogs:
|
||||||
|
if cog.repo is None:
|
||||||
|
continue
|
||||||
|
repos.add(cog.repo)
|
||||||
|
cogs.append(cog)
|
||||||
|
failed_reqs = await _install_requirements(cogs)
|
||||||
|
all_installed_libs: List[InstalledModule] = []
|
||||||
|
all_failed_libs: List[Installable] = []
|
||||||
|
for repo in repos:
|
||||||
|
installed_libs, failed_libs = await repo.install_libraries(
|
||||||
|
target_dir=SHAREDLIB_PATH, req_target_dir=LIB_PATH
|
||||||
|
)
|
||||||
|
all_installed_libs += installed_libs
|
||||||
|
all_failed_libs += failed_libs
|
||||||
|
|
||||||
|
return failed_reqs, all_failed_libs
|
||||||
|
|
||||||
|
|
||||||
|
async def install_cogs(
|
||||||
|
repo: Repo, rev: Optional[str], cog_names: Iterable[str]
|
||||||
|
) -> CogInstallResult:
|
||||||
|
commit = None
|
||||||
|
|
||||||
|
if rev is not None:
|
||||||
|
# raises errors.AmbiguousRevision and errors.UnknownRevision
|
||||||
|
commit = await repo.get_full_sha1(rev)
|
||||||
|
|
||||||
|
cog_names = set(cog_names)
|
||||||
|
_installed_cogs = await installed_cogs()
|
||||||
|
|
||||||
|
cogs: List[Installable] = []
|
||||||
|
unavailable_cogs: List[str] = []
|
||||||
|
already_installed: List[Installable] = []
|
||||||
|
name_already_used: List[Installable] = []
|
||||||
|
incompatible_python_version: List[Installable] = []
|
||||||
|
incompatible_bot_version: List[Installable] = []
|
||||||
|
|
||||||
|
result_installed_cogs: Tuple[InstalledModule, ...] = ()
|
||||||
|
result_failed_cogs: Tuple[Installable, ...] = ()
|
||||||
|
result_failed_reqs: Tuple[str, ...] = ()
|
||||||
|
result_installed_libs: Tuple[InstalledModule, ...] = ()
|
||||||
|
result_failed_libs: Tuple[Installable, ...] = ()
|
||||||
|
|
||||||
|
async with repo.checkout(commit, exit_to_rev=repo.branch):
|
||||||
|
for cog_name in cog_names:
|
||||||
|
cog: Optional[Installable] = discord.utils.get(repo.available_cogs, name=cog_name)
|
||||||
|
if cog is None:
|
||||||
|
unavailable_cogs.append(cog_name)
|
||||||
|
elif cog in _installed_cogs:
|
||||||
|
already_installed.append(cog)
|
||||||
|
elif discord.utils.get(_installed_cogs, name=cog.name):
|
||||||
|
name_already_used.append(cog)
|
||||||
|
elif cog.min_python_version > sys.version_info:
|
||||||
|
incompatible_python_version.append(cog)
|
||||||
|
elif cog.min_bot_version > red_version_info or (
|
||||||
|
# max version should be ignored when it's lower than min version
|
||||||
|
cog.min_bot_version <= cog.max_bot_version
|
||||||
|
and cog.max_bot_version < red_version_info
|
||||||
|
):
|
||||||
|
incompatible_bot_version.append(cog)
|
||||||
|
else:
|
||||||
|
cogs.append(cog)
|
||||||
|
|
||||||
|
if cogs:
|
||||||
|
result_failed_reqs = await _install_requirements(cogs)
|
||||||
|
if not result_failed_reqs:
|
||||||
|
result_installed_cogs, result_failed_cogs = await _install_cogs(cogs)
|
||||||
|
|
||||||
|
if cogs and not result_failed_reqs:
|
||||||
|
result_installed_libs, result_failed_libs = await repo.install_libraries(
|
||||||
|
target_dir=SHAREDLIB_PATH, req_target_dir=LIB_PATH
|
||||||
|
)
|
||||||
|
if rev is not None:
|
||||||
|
for cog in result_installed_cogs:
|
||||||
|
cog.pinned = True
|
||||||
|
await _save_to_installed(result_installed_cogs + result_installed_libs)
|
||||||
|
|
||||||
|
return CogInstallResult(
|
||||||
|
installed_cogs=result_installed_cogs,
|
||||||
|
installed_libs=result_installed_libs,
|
||||||
|
failed_cogs=result_failed_cogs,
|
||||||
|
failed_libs=result_failed_libs,
|
||||||
|
failed_reqs=result_failed_reqs,
|
||||||
|
unavailable_cogs=tuple(unavailable_cogs),
|
||||||
|
already_installed=tuple(already_installed),
|
||||||
|
name_already_used=tuple(name_already_used),
|
||||||
|
incompatible_python_version=tuple(incompatible_python_version),
|
||||||
|
incompatible_bot_version=tuple(incompatible_bot_version),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def uninstall_cogs(*cogs: InstalledModule) -> tuple[list[str], list[str]]:
|
||||||
|
uninstalled_cogs = []
|
||||||
|
failed_cogs = []
|
||||||
|
for cog in set(cogs):
|
||||||
|
real_name = cog.name
|
||||||
|
|
||||||
|
poss_installed_path = (await _cog_mgr.install_path()) / real_name
|
||||||
|
if poss_installed_path.exists():
|
||||||
|
if _bot_ref is not None:
|
||||||
|
with contextlib.suppress(commands.ExtensionNotLoaded):
|
||||||
|
await _bot_ref.unload_extension(real_name)
|
||||||
|
await _bot_ref.remove_loaded_package(real_name)
|
||||||
|
await _delete_cog(poss_installed_path)
|
||||||
|
uninstalled_cogs.append(real_name)
|
||||||
|
else:
|
||||||
|
failed_cogs.append(real_name)
|
||||||
|
await _remove_from_installed(cogs)
|
||||||
|
|
||||||
|
return uninstalled_cogs, failed_cogs
|
||||||
|
|
||||||
|
|
||||||
|
async def check_cog_updates(
|
||||||
|
*,
|
||||||
|
repos: Optional[Iterable[Repo]] = None,
|
||||||
|
cogs: Optional[Iterable[InstalledModule]] = None,
|
||||||
|
update_repos: bool = True,
|
||||||
|
) -> CogUpdateCheckResult:
|
||||||
|
cogs_to_check, failed_repos = await _get_cogs_to_check(
|
||||||
|
repos=repos, cogs=cogs, update_repos=update_repos
|
||||||
|
)
|
||||||
|
outdated_cogs, outdated_libs = await _available_updates(cogs_to_check)
|
||||||
|
|
||||||
|
updatable_cogs: List[Installable] = []
|
||||||
|
incompatible_python_version: List[Installable] = []
|
||||||
|
incompatible_bot_version: List[Installable] = []
|
||||||
|
for cog in outdated_cogs:
|
||||||
|
if cog.min_python_version > sys.version_info:
|
||||||
|
incompatible_python_version.append(cog)
|
||||||
|
elif cog.min_bot_version > red_version_info or (
|
||||||
|
# max version should be ignored when it's lower than min version
|
||||||
|
cog.min_bot_version <= cog.max_bot_version
|
||||||
|
and cog.max_bot_version < red_version_info
|
||||||
|
):
|
||||||
|
incompatible_bot_version.append(cog)
|
||||||
|
else:
|
||||||
|
updatable_cogs.append(cog)
|
||||||
|
|
||||||
|
return CogUpdateCheckResult(
|
||||||
|
outdated_cogs=outdated_cogs,
|
||||||
|
outdated_libs=outdated_libs,
|
||||||
|
updatable_cogs=tuple(updatable_cogs),
|
||||||
|
failed_repos=tuple(failed_repos),
|
||||||
|
incompatible_python_version=tuple(incompatible_python_version),
|
||||||
|
incompatible_bot_version=tuple(incompatible_bot_version),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# update given cogs or all cogs
|
||||||
|
async def update_cogs(
|
||||||
|
*, cogs: Optional[List[InstalledModule]] = None, repos: Optional[List[Repo]] = None
|
||||||
|
) -> CogUpdateResult:
|
||||||
|
if cogs is not None and repos is not None:
|
||||||
|
raise ValueError("You can specify cogs or repos argument, not both")
|
||||||
|
|
||||||
|
cogs_to_check, failed_repos = await _get_cogs_to_check(repos=repos, cogs=cogs)
|
||||||
|
return await _update_cogs(cogs_to_check, failed_repos=failed_repos)
|
||||||
|
|
||||||
|
|
||||||
|
# update given cogs or all cogs from the specified repo
|
||||||
|
# using the specified revision (or latest if not specified)
|
||||||
|
async def update_repo_cogs(
|
||||||
|
repo: Repo, cogs: Optional[List[InstalledModule]] = None, *, rev: Optional[str] = None
|
||||||
|
) -> CogUpdateResult:
|
||||||
|
try:
|
||||||
|
await repo.update()
|
||||||
|
except errors.UpdateError:
|
||||||
|
return await _update_cogs(set(), failed_repos=[repo])
|
||||||
|
|
||||||
|
# TODO: should this be set to `repo.branch` when `rev` is None?
|
||||||
|
commit = None
|
||||||
|
if rev is not None:
|
||||||
|
# raises errors.AmbiguousRevision and errors.UnknownRevision
|
||||||
|
commit = await repo.get_full_sha1(rev)
|
||||||
|
async with repo.checkout(commit, exit_to_rev=repo.branch):
|
||||||
|
cogs_to_check, __ = await _get_cogs_to_check(repos=[repo], cogs=cogs, update_repos=False)
|
||||||
|
return await _update_cogs(cogs_to_check, failed_repos=())
|
||||||
|
|
||||||
|
|
||||||
|
async def _update_cogs(
|
||||||
|
cogs_to_check: Set[InstalledModule], *, failed_repos: Sequence[Repo]
|
||||||
|
) -> CogUpdateResult:
|
||||||
|
pinned_cogs = {cog for cog in cogs_to_check if cog.pinned}
|
||||||
|
cogs_to_check -= pinned_cogs
|
||||||
|
|
||||||
|
outdated_cogs: Tuple[Installable, ...] = ()
|
||||||
|
outdated_libs: Tuple[Installable, ...] = ()
|
||||||
|
updatable_cogs: List[Installable] = []
|
||||||
|
incompatible_python_version: List[Installable] = []
|
||||||
|
incompatible_bot_version: List[Installable] = []
|
||||||
|
|
||||||
|
updated_cogs: Tuple[InstalledModule, ...] = ()
|
||||||
|
failed_cogs: Tuple[Installable, ...] = ()
|
||||||
|
failed_reqs: Tuple[str, ...] = ()
|
||||||
|
updated_libs: Tuple[InstalledModule, ...] = ()
|
||||||
|
failed_libs: Tuple[Installable, ...] = ()
|
||||||
|
|
||||||
|
if cogs_to_check:
|
||||||
|
outdated_cogs, outdated_libs = await _available_updates(cogs_to_check)
|
||||||
|
|
||||||
|
for cog in outdated_cogs:
|
||||||
|
if cog.min_python_version > sys.version_info:
|
||||||
|
incompatible_python_version.append(cog)
|
||||||
|
elif cog.min_bot_version > red_version_info or (
|
||||||
|
# max version should be ignored when it's lower than min version
|
||||||
|
cog.min_bot_version <= cog.max_bot_version
|
||||||
|
and cog.max_bot_version < red_version_info
|
||||||
|
):
|
||||||
|
incompatible_bot_version.append(cog)
|
||||||
|
else:
|
||||||
|
updatable_cogs.append(cog)
|
||||||
|
|
||||||
|
if updatable_cogs or outdated_libs:
|
||||||
|
failed_reqs = await _install_requirements(updatable_cogs)
|
||||||
|
if not failed_reqs:
|
||||||
|
updated_cogs, failed_cogs = await _install_cogs(updatable_cogs)
|
||||||
|
updated_libs, failed_libs = await _reinstall_libraries(outdated_libs)
|
||||||
|
await _save_to_installed(updated_cogs + updated_libs)
|
||||||
|
|
||||||
|
return CogUpdateResult(
|
||||||
|
checked_cogs=frozenset(cogs_to_check),
|
||||||
|
pinned_cogs=frozenset(pinned_cogs),
|
||||||
|
updated_cogs=updated_cogs,
|
||||||
|
updated_libs=updated_libs,
|
||||||
|
failed_cogs=failed_cogs,
|
||||||
|
failed_libs=failed_libs,
|
||||||
|
failed_reqs=failed_reqs,
|
||||||
|
outdated_cogs=outdated_cogs,
|
||||||
|
outdated_libs=outdated_libs,
|
||||||
|
updatable_cogs=tuple(updatable_cogs),
|
||||||
|
failed_repos=tuple(failed_repos),
|
||||||
|
incompatible_python_version=tuple(incompatible_python_version),
|
||||||
|
incompatible_bot_version=tuple(incompatible_bot_version),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def pin_cogs(
|
||||||
|
*cogs: InstalledModule,
|
||||||
|
) -> tuple[tuple[InstalledModule, ...], tuple[InstalledModule, ...]]:
|
||||||
|
already_pinned = []
|
||||||
|
pinned = []
|
||||||
|
for cog in set(cogs):
|
||||||
|
if cog.pinned:
|
||||||
|
already_pinned.append(cog)
|
||||||
|
continue
|
||||||
|
cog.pinned = True
|
||||||
|
pinned.append(cog)
|
||||||
|
if pinned:
|
||||||
|
await _save_to_installed(pinned)
|
||||||
|
|
||||||
|
return tuple(pinned), tuple(already_pinned)
|
||||||
|
|
||||||
|
|
||||||
|
async def unpin_cogs(
|
||||||
|
*cogs: InstalledModule,
|
||||||
|
) -> tuple[tuple[InstalledModule, ...], tuple[InstalledModule, ...]]:
|
||||||
|
not_pinned = []
|
||||||
|
unpinned = []
|
||||||
|
for cog in set(cogs):
|
||||||
|
if not cog.pinned:
|
||||||
|
not_pinned.append(cog)
|
||||||
|
continue
|
||||||
|
cog.pinned = False
|
||||||
|
unpinned.append(cog)
|
||||||
|
if unpinned:
|
||||||
|
await _save_to_installed(unpinned)
|
||||||
|
|
||||||
|
return tuple(unpinned), tuple(not_pinned)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: make kw_only
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class CogInstallResult:
|
||||||
|
installed_cogs: Tuple[InstalledModule, ...]
|
||||||
|
installed_libs: Tuple[InstalledModule, ...]
|
||||||
|
failed_cogs: Tuple[Installable, ...]
|
||||||
|
failed_libs: Tuple[Installable, ...]
|
||||||
|
failed_reqs: Tuple[str, ...]
|
||||||
|
unavailable_cogs: Tuple[str, ...]
|
||||||
|
already_installed: Tuple[Installable, ...]
|
||||||
|
name_already_used: Tuple[Installable, ...]
|
||||||
|
incompatible_python_version: Tuple[Installable, ...]
|
||||||
|
incompatible_bot_version: Tuple[Installable, ...]
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: make kw_only
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class CogUpdateCheckResult:
|
||||||
|
outdated_cogs: Tuple[Installable, ...]
|
||||||
|
outdated_libs: Tuple[Installable, ...]
|
||||||
|
updatable_cogs: Tuple[Installable, ...]
|
||||||
|
failed_repos: Tuple[Repo, ...]
|
||||||
|
incompatible_python_version: Tuple[Installable, ...]
|
||||||
|
incompatible_bot_version: Tuple[Installable, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def updates_available(self) -> bool:
|
||||||
|
return bool(self.outdated_cogs or self.outdated_libs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def updates_installable(self) -> bool:
|
||||||
|
return bool(self.updatable_cogs or self.outdated_libs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def incompatible_cogs(self) -> Tuple[Installable, ...]:
|
||||||
|
return self.incompatible_python_version + self.incompatible_bot_version
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: make kw_only
|
||||||
|
@dataclasses.dataclass
|
||||||
|
class CogUpdateResult(CogUpdateCheckResult):
|
||||||
|
# checked_cogs contains old modules, before update
|
||||||
|
checked_cogs: Set[InstalledModule]
|
||||||
|
pinned_cogs: Set[InstalledModule]
|
||||||
|
updated_cogs: Tuple[InstalledModule, ...]
|
||||||
|
updated_libs: Tuple[InstalledModule, ...]
|
||||||
|
failed_cogs: Tuple[Installable, ...]
|
||||||
|
failed_libs: Tuple[Installable, ...]
|
||||||
|
failed_reqs: Tuple[str, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def updated_modules(self) -> Tuple[InstalledModule, ...]:
|
||||||
|
return self.updated_cogs + self.updated_libs
|
||||||
|
|
||||||
|
|
||||||
|
class CogUnavailableError(Exception):
|
||||||
|
def __init__(self, repo_name: str, cog_name: str) -> None:
|
||||||
|
self.repo_name = repo_name
|
||||||
|
self.cog_name = cog_name
|
||||||
|
super().__init__(f"Couldn't find cog {cog_name!r} in {repo_name!r}")
|
||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import functools
|
import functools
|
||||||
import shutil
|
import shutil
|
||||||
from enum import IntEnum
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Union, cast
|
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, Tuple, Union, cast
|
||||||
|
|
||||||
@@ -16,8 +16,7 @@ if TYPE_CHECKING:
|
|||||||
from .repo_manager import RepoManager, Repo
|
from .repo_manager import RepoManager, Repo
|
||||||
|
|
||||||
|
|
||||||
class InstallableType(IntEnum):
|
class InstallableType(Enum):
|
||||||
# using IntEnum, because hot-reload breaks its identity
|
|
||||||
UNKNOWN = 0
|
UNKNOWN = 0
|
||||||
COG = 1
|
COG = 1
|
||||||
SHARED_LIBRARY = 2
|
SHARED_LIBRARY = 2
|
||||||
@@ -139,7 +138,7 @@ class Installable(RepoJSONMixin):
|
|||||||
super()._read_info_file()
|
super()._read_info_file()
|
||||||
|
|
||||||
update_mixin(self, INSTALLABLE_SCHEMA)
|
update_mixin(self, INSTALLABLE_SCHEMA)
|
||||||
if self.type == InstallableType.SHARED_LIBRARY:
|
if self.type is InstallableType.SHARED_LIBRARY:
|
||||||
self.hidden = True
|
self.hidden = True
|
||||||
|
|
||||||
|
|
||||||
@@ -163,7 +162,7 @@ class InstalledModule(Installable):
|
|||||||
json_repo_name: str = "",
|
json_repo_name: str = "",
|
||||||
):
|
):
|
||||||
super().__init__(location=location, repo=repo, commit=commit)
|
super().__init__(location=location, repo=repo, commit=commit)
|
||||||
self.pinned: bool = pinned if self.type == InstallableType.COG else False
|
self.pinned: bool = pinned if self.type is InstallableType.COG else False
|
||||||
# this is here so that Downloader could use real repo name instead of "MISSING_REPO"
|
# this is here so that Downloader could use real repo name instead of "MISSING_REPO"
|
||||||
self._json_repo_name = json_repo_name
|
self._json_repo_name = json_repo_name
|
||||||
|
|
||||||
@@ -173,7 +172,7 @@ class InstalledModule(Installable):
|
|||||||
"module_name": self.name,
|
"module_name": self.name,
|
||||||
"commit": self.commit,
|
"commit": self.commit,
|
||||||
}
|
}
|
||||||
if self.type == InstallableType.COG:
|
if self.type is InstallableType.COG:
|
||||||
module_json["pinned"] = self.pinned
|
module_json["pinned"] = self.pinned
|
||||||
return module_json
|
return module_json
|
||||||
|
|
||||||
3
redbot/core/_downloader/log.py
Normal file
3
redbot/core/_downloader/log.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
log = logging.getLogger("red.core.downloader")
|
||||||
@@ -169,21 +169,6 @@ class Repo(RepoJSONMixin):
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return self.url
|
return self.url
|
||||||
|
|
||||||
@classmethod
|
|
||||||
async def convert(cls, ctx: commands.Context, argument: str) -> Repo:
|
|
||||||
downloader_cog = ctx.bot.get_cog("Downloader")
|
|
||||||
if downloader_cog is None:
|
|
||||||
raise commands.CommandError(_("No Downloader cog found."))
|
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
|
||||||
repo_manager = downloader_cog._repo_manager
|
|
||||||
poss_repo = repo_manager.get_repo(argument)
|
|
||||||
if poss_repo is None:
|
|
||||||
raise commands.BadArgument(
|
|
||||||
_('Repo by the name "{repo_name}" does not exist.').format(repo_name=argument)
|
|
||||||
)
|
|
||||||
return poss_repo
|
|
||||||
|
|
||||||
def _existing_git_repo(self) -> Tuple[bool, Path]:
|
def _existing_git_repo(self) -> Tuple[bool, Path]:
|
||||||
git_path = self.folder_path / ".git"
|
git_path = self.folder_path / ".git"
|
||||||
return git_path.exists(), git_path
|
return git_path.exists(), git_path
|
||||||
@@ -1001,7 +986,7 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return tuple(
|
return tuple(
|
||||||
[m for m in self.available_modules if m.type == InstallableType.COG and not m.disabled]
|
[m for m in self.available_modules if m.type is InstallableType.COG and not m.disabled]
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -1011,7 +996,7 @@ class Repo(RepoJSONMixin):
|
|||||||
"""
|
"""
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return tuple(
|
return tuple(
|
||||||
[m for m in self.available_modules if m.type == InstallableType.SHARED_LIBRARY]
|
[m for m in self.available_modules if m.type is InstallableType.SHARED_LIBRARY]
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -37,7 +37,18 @@ import discord
|
|||||||
from discord.ext import commands as dpy_commands
|
from discord.ext import commands as dpy_commands
|
||||||
from discord.ext.commands import when_mentioned_or
|
from discord.ext.commands import when_mentioned_or
|
||||||
|
|
||||||
from . import Config, _i18n, i18n, app_commands, commands, errors, _drivers, modlog, bank
|
from . import (
|
||||||
|
Config,
|
||||||
|
_i18n,
|
||||||
|
i18n,
|
||||||
|
app_commands,
|
||||||
|
commands,
|
||||||
|
errors,
|
||||||
|
_drivers,
|
||||||
|
modlog,
|
||||||
|
bank,
|
||||||
|
_downloader,
|
||||||
|
)
|
||||||
from ._cli import ExitCodes
|
from ._cli import ExitCodes
|
||||||
from ._cog_manager import CogManager, CogManagerUI
|
from ._cog_manager import CogManager, CogManagerUI
|
||||||
from .core_commands import Core
|
from .core_commands import Core
|
||||||
@@ -1200,12 +1211,11 @@ class Red(
|
|||||||
|
|
||||||
ver_info = list(sys.version_info[:2])
|
ver_info = list(sys.version_info[:2])
|
||||||
python_version_changed = False
|
python_version_changed = False
|
||||||
LIB_PATH = cog_data_path(raw_name="Downloader") / "lib"
|
|
||||||
if ver_info != last_system_info["python_version"]:
|
if ver_info != last_system_info["python_version"]:
|
||||||
await self._config.last_system_info.python_version.set(ver_info)
|
await self._config.last_system_info.python_version.set(ver_info)
|
||||||
if any(LIB_PATH.iterdir()):
|
if any(_downloader.LIB_PATH.iterdir()):
|
||||||
shutil.rmtree(str(LIB_PATH))
|
shutil.rmtree(str(_downloader.LIB_PATH))
|
||||||
LIB_PATH.mkdir()
|
_downloader.LIB_PATH.mkdir()
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
send_to_owners_with_prefix_replaced(
|
send_to_owners_with_prefix_replaced(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ from . import (
|
|||||||
i18n,
|
i18n,
|
||||||
bank,
|
bank,
|
||||||
modlog,
|
modlog,
|
||||||
|
_downloader,
|
||||||
)
|
)
|
||||||
from ._diagnoser import IssueDiagnoser
|
from ._diagnoser import IssueDiagnoser
|
||||||
from .utils import AsyncIter, can_user_send_messages_in
|
from .utils import AsyncIter, can_user_send_messages_in
|
||||||
@@ -215,12 +216,8 @@ 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.4
|
|
||||||
downloader = bot.get_cog("Downloader")
|
|
||||||
if downloader is None:
|
|
||||||
continue
|
|
||||||
try:
|
try:
|
||||||
maybe_repo = await downloader._shared_lib_load_check(name)
|
maybe_repo = await _downloader._shared_lib_load_check(name)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception(
|
log.exception(
|
||||||
"Shared library check failed,"
|
"Shared library check failed,"
|
||||||
|
|||||||
@@ -236,7 +236,7 @@ async def create_backup(dest: Path = Path.home()) -> Optional[Path]:
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Avoiding circular imports
|
# Avoiding circular imports
|
||||||
from ...cogs.downloader.repo_manager import RepoManager
|
from redbot.core._downloader.repo_manager import RepoManager
|
||||||
|
|
||||||
repo_mgr = RepoManager()
|
repo_mgr = RepoManager()
|
||||||
await repo_mgr.initialize()
|
await repo_mgr.initialize()
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import shutil
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redbot.cogs.downloader.repo_manager import RepoManager, Repo, ProcessFormatter
|
from redbot.core._downloader.repo_manager import RepoManager, Repo, ProcessFormatter
|
||||||
from redbot.cogs.downloader.installable import Installable, InstalledModule
|
from redbot.core._downloader.installable import Installable, InstalledModule
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"GIT_VERSION",
|
"GIT_VERSION",
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ from pytest_mock import MockFixture
|
|||||||
|
|
||||||
from redbot.pytest.downloader import *
|
from redbot.pytest.downloader import *
|
||||||
|
|
||||||
from redbot.cogs.downloader.repo_manager import Installable
|
from redbot.core._downloader.repo_manager import Installable
|
||||||
from redbot.cogs.downloader.repo_manager import Candidate, ProcessFormatter, RepoManager, Repo
|
from redbot.core._downloader.repo_manager import Candidate, ProcessFormatter, RepoManager, Repo
|
||||||
from redbot.cogs.downloader.errors import (
|
from redbot.core._downloader.errors import (
|
||||||
AmbiguousRevision,
|
AmbiguousRevision,
|
||||||
ExistingGitRepo,
|
ExistingGitRepo,
|
||||||
GitException,
|
GitException,
|
||||||
@@ -322,9 +322,9 @@ async def test_update(mocker, repo):
|
|||||||
|
|
||||||
|
|
||||||
async def test_add_repo(monkeypatch, repo_manager):
|
async def test_add_repo(monkeypatch, repo_manager):
|
||||||
monkeypatch.setattr("redbot.cogs.downloader.repo_manager.Repo._run", fake_run_noprint)
|
monkeypatch.setattr("redbot.core._downloader.repo_manager.Repo._run", fake_run_noprint)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"redbot.cogs.downloader.repo_manager.Repo.current_commit", fake_current_commit
|
"redbot.core._downloader.repo_manager.Repo.current_commit", fake_current_commit
|
||||||
)
|
)
|
||||||
|
|
||||||
squid = await repo_manager.add_repo(
|
squid = await repo_manager.add_repo(
|
||||||
@@ -335,9 +335,9 @@ async def test_add_repo(monkeypatch, repo_manager):
|
|||||||
|
|
||||||
|
|
||||||
async def test_lib_install_requirements(monkeypatch, library_installable, repo, tmpdir):
|
async def test_lib_install_requirements(monkeypatch, library_installable, repo, tmpdir):
|
||||||
monkeypatch.setattr("redbot.cogs.downloader.repo_manager.Repo._run", fake_run_noprint)
|
monkeypatch.setattr("redbot.core._downloader.repo_manager.Repo._run", fake_run_noprint)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"redbot.cogs.downloader.repo_manager.Repo.available_libraries", (library_installable,)
|
"redbot.core._downloader.repo_manager.Repo.available_libraries", (library_installable,)
|
||||||
)
|
)
|
||||||
|
|
||||||
lib_path = Path(str(tmpdir)) / "cog_data_path" / "lib"
|
lib_path = Path(str(tmpdir)) / "cog_data_path" / "lib"
|
||||||
@@ -353,9 +353,9 @@ async def test_lib_install_requirements(monkeypatch, library_installable, repo,
|
|||||||
|
|
||||||
|
|
||||||
async def test_remove_repo(monkeypatch, repo_manager):
|
async def test_remove_repo(monkeypatch, repo_manager):
|
||||||
monkeypatch.setattr("redbot.cogs.downloader.repo_manager.Repo._run", fake_run_noprint)
|
monkeypatch.setattr("redbot.core._downloader.repo_manager.Repo._run", fake_run_noprint)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
"redbot.cogs.downloader.repo_manager.Repo.current_commit", fake_current_commit
|
"redbot.core._downloader.repo_manager.Repo.current_commit", fake_current_commit
|
||||||
)
|
)
|
||||||
|
|
||||||
await repo_manager.add_repo(
|
await repo_manager.add_repo(
|
||||||
@@ -3,7 +3,7 @@ import subprocess as sp
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redbot.cogs.downloader.repo_manager import ProcessFormatter, Repo
|
from redbot.core._downloader.repo_manager import ProcessFormatter, Repo
|
||||||
from redbot.pytest.downloader import (
|
from redbot.pytest.downloader import (
|
||||||
GIT_VERSION,
|
GIT_VERSION,
|
||||||
cloned_git_repo,
|
cloned_git_repo,
|
||||||
@@ -4,14 +4,14 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from redbot.pytest.downloader import *
|
from redbot.pytest.downloader import *
|
||||||
from redbot.cogs.downloader.installable import Installable, InstallableType
|
from redbot.core._downloader.installable import Installable, InstallableType
|
||||||
from redbot.core import VersionInfo
|
from redbot.core import VersionInfo
|
||||||
|
|
||||||
|
|
||||||
def test_process_info_file(installable):
|
def test_process_info_file(installable):
|
||||||
for k, v in INFO_JSON.items():
|
for k, v in INFO_JSON.items():
|
||||||
if k == "type":
|
if k == "type":
|
||||||
assert installable.type == InstallableType.COG
|
assert installable.type is InstallableType.COG
|
||||||
elif k in ("min_bot_version", "max_bot_version"):
|
elif k in ("min_bot_version", "max_bot_version"):
|
||||||
assert getattr(installable, k) == VersionInfo.from_str(v)
|
assert getattr(installable, k) == VersionInfo.from_str(v)
|
||||||
else:
|
else:
|
||||||
@@ -21,7 +21,7 @@ def test_process_info_file(installable):
|
|||||||
def test_process_lib_info_file(library_installable):
|
def test_process_lib_info_file(library_installable):
|
||||||
for k, v in LIBRARY_INFO_JSON.items():
|
for k, v in LIBRARY_INFO_JSON.items():
|
||||||
if k == "type":
|
if k == "type":
|
||||||
assert library_installable.type == InstallableType.SHARED_LIBRARY
|
assert library_installable.type is InstallableType.SHARED_LIBRARY
|
||||||
elif k in ("min_bot_version", "max_bot_version"):
|
elif k in ("min_bot_version", "max_bot_version"):
|
||||||
assert getattr(library_installable, k) == VersionInfo.from_str(v)
|
assert getattr(library_installable, k) == VersionInfo.from_str(v)
|
||||||
elif k == "hidden":
|
elif k == "hidden":
|
||||||
Reference in New Issue
Block a user