Red-DiscordBot/cogs/downloader/installable.py
2017-06-18 01:31:32 +02:00

191 lines
6.1 KiB
Python

import json
import distutils.dir_util
import shutil
from enum import Enum
from pathlib import Path
from typing import Union, MutableMapping, Any
from .log import log
from .json_mixins import RepoJSONMixin
class InstallableType(Enum):
UNKNOWN = 0
COG = 1
SHARED_LIBRARY = 2
class Installable(RepoJSONMixin):
"""
Base class for anything the Downloader cog can install.
- Modules
- Repo Libraries
- Other stuff?
"""
INFO_FILE_DESCRIPTION = """
The info.json file may exist inside every package folder in the repo,
it is optional however. This string describes the valid keys within
an info file (and maybe how the Downloader cog uses them).
KEYS (case sensitive):
author (list of strings) - list of names of authors of the cog
bot_version (list of integer) - Min version number of Red in the
format (MAJOR, MINOR, PATCH)
description (string) - A long description of the cog that appears
when a user executes `!cog info`
hidden (bool) - Determines if a cog is available for install.
install_msg (string) - The message that gets displayed when a cog is
installed
required_cogs (map of cogname to repo URL) - A map of required cogs
that this cog depends on. Downloader will not deal with this
functionality but it may be useful for other cogs.
requirements (list of strings) - list of required libraries that are
passed to pip on cog install. SHARED_LIBRARIES do NOT go in this
list.
short (string) - A short description of the cog that appears when
a user executes `!cog list`
tags (list of strings) - A list of strings that are related to the
functionality of the cog. Used to aid in searching.
type (string) - Optional, defaults to COG. Must be either COG or
SHARED_LIBRARY. If SHARED_LIBRARY then HIDDEN will be True.
"""
def __init__(self, location: Path):
"""
Base installable initializer.
:param location: Location (file or folder) to the installable.
"""
super().__init__(location)
self._location = location
self.repo_name = self._location.parent.stem
self.author = ()
self.bot_version = (3, 0, 0)
self.hidden = False
self.required_cogs = {} # Cog name -> repo URL
self.requirements = ()
self.tags = ()
self.type = InstallableType.UNKNOWN
if self._info_file.exists():
self._process_info_file(self._info_file)
if self._info == {}:
self.type = InstallableType.COG
def __eq__(self, other):
# noinspection PyProtectedMember
return self._location == other._location
def __hash__(self):
return hash(self._location)
@property
def name(self):
return self._location.stem
async def copy_to(self, target_dir: Path) -> bool:
"""
Copies this cog/shared_lib to the given directory. This
will overwrite any files in the target directory
:param target_dir: The installation directory to install to.
:return: bool - status of installation
"""
if self._location.is_file():
copy_func = shutil.copy2
else:
copy_func = distutils.dir_util.copy_tree
# noinspection PyBroadException
try:
copy_func(
src=str(self._location),
dst=str(target_dir / self._location.stem)
)
except:
log.exception("Error occurred when copying path:"
" {}".format(self._location))
return False
return True
def _read_info_file(self):
super()._read_info_file()
if self._info_file.exists():
self._process_info_file()
def _process_info_file(self, info_file_path: Path=None) -> MutableMapping[str, Any]:
"""
Processes an information file. Loads dependencies among other
information into this object.
:type info_file_path:
:param info_file_path: Optional path to information file, defaults to `self.__info_file`
:return: Raw information dictionary
"""
info_file_path = info_file_path or self._info_file
if info_file_path is None or not info_file_path.is_file():
raise ValueError("No valid information file path was found.")
info = {}
with info_file_path.open(encoding='utf-8') as f:
try:
info = json.load(f)
except json.JSONDecodeError:
info = {}
log.exception("Invalid JSON information file at path:"
" {}".format(info_file_path))
else:
self._info = info
try:
author = tuple(info.get("author", []))
except ValueError:
author = ()
self.author = author
try:
bot_version = tuple(info.get("bot_version", [3, 0, 0]))
except ValueError:
bot_version = 2
self.bot_version = bot_version
try:
hidden = bool(info.get("hidden", False))
except ValueError:
hidden = False
self.hidden = hidden
self.required_cogs = info.get("required_cogs", {})
self.requirements = info.get("requirements", ())
try:
tags = tuple(info.get("tags", ()))
except ValueError:
tags = ()
self.tags = tags
installable_type = info.get("type", "")
if installable_type in ("", "COG"):
self.type = InstallableType.COG
elif installable_type == "SHARED_LIBRARY":
self.type = InstallableType.SHARED_LIBRARY
self.hidden = True
else:
self.type = InstallableType.UNKNOWN
return info
def to_json(self):
return {
"location": self._location.relative_to(Path.cwd()).parts
}
@classmethod
def from_json(cls, data: dict):
location = Path.cwd() / Path(*data["location"])
return cls(location=location)