[V3 Cog/Data Manager] Bundled Cog Data (#1063)

* Refactor find_spec out of core_commands

* Fix version error when not installed

* initial

* Fix find_cogs call

* Enable copying

* Add helper method for cog creators

* Add warning

* My dpy skillz need work
This commit is contained in:
Will 2017-10-27 20:06:47 -04:00 committed by GitHub
parent 77e29ff43b
commit 09b3642559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 227 additions and 35 deletions

View File

@ -5,4 +5,7 @@ from .context import RedContext
__all__ = ["Config", "RedContext", "__version__"] __all__ = ["Config", "RedContext", "__version__"]
__version__ = version = pkg_resources.require("Red-DiscordBot")[0].version try:
__version__ = pkg_resources.require("Red-DiscordBot")[0].version
except pkg_resources.DistributionNotFound:
__version__ = "3.0.0"

View File

@ -1,8 +1,9 @@
import contextlib
import pkgutil import pkgutil
from importlib import invalidate_caches from importlib import import_module, invalidate_caches
from importlib.machinery import ModuleSpec from importlib.machinery import ModuleSpec
from pathlib import Path from pathlib import Path
from typing import Tuple, Union, List from typing import Tuple, Union, List, overload
import redbot.cogs import redbot.cogs
@ -187,6 +188,61 @@ class CogManager:
str_paths = [str(p) for p in paths_] str_paths = [str(p) for p in paths_]
await self.conf.paths.set(str_paths) await self.conf.paths.set(str_paths)
async def _find_ext_cog(self, name: str) -> ModuleSpec:
"""
Attempts to find a spec for a third party installed cog.
Parameters
----------
name : str
Name of the cog package to look for.
Returns
-------
importlib.machinery.ModuleSpec
Module spec to be used for cog loading.
Raises
------
RuntimeError
When no matching spec can be found.
"""
resolved_paths = [str(p.resolve()) for p in await self.paths()]
for finder, module_name, _ in pkgutil.iter_modules(resolved_paths):
if name == module_name:
spec = finder.find_spec(name)
if spec:
return spec
raise RuntimeError("No 3rd party module by the name of '{}' was found"
" in any available path.".format(name))
async def _find_core_cog(self, name: str) -> ModuleSpec:
"""
Attempts to find a spec for a core cog.
Parameters
----------
name : str
Returns
-------
importlib.machinery.ModuleSpec
Raises
------
RuntimeError
When no matching spec can be found.
"""
real_name = ".{}".format(name)
try:
mod = import_module(real_name, package='redbot.cogs')
except ImportError as e:
raise RuntimeError("No core cog by the name of '{}' could"
"be found.".format(name)) from e
return mod.__spec__
# noinspection PyUnreachableCode
async def find_cog(self, name: str) -> ModuleSpec: async def find_cog(self, name: str) -> ModuleSpec:
"""Find a cog in the list of available paths. """Find a cog in the list of available paths.
@ -206,15 +262,13 @@ class CogManager:
If there is no cog with the given name. If there is no cog with the given name.
""" """
resolved_paths = [str(p.resolve()) for p in await self.paths()] with contextlib.suppress(RuntimeError):
for finder, module_name, _ in pkgutil.iter_modules(resolved_paths): return await self._find_ext_cog(name)
if name == module_name:
spec = finder.find_spec(name)
if spec:
return spec
raise RuntimeError("No module by the name of '{}' was found" with contextlib.suppress(RuntimeError):
" in any available path.".format(name)) return await self._find_core_cog(name)
raise RuntimeError("No cog with that name could be found.")
async def available_modules(self) -> List[str]: async def available_modules(self) -> List[str]:
"""Finds the names of all available modules to load. """Finds the names of all available modules to load.

View File

@ -14,9 +14,7 @@ from discord.ext import commands
from redbot.core import checks from redbot.core import checks
from redbot.core import i18n from redbot.core import i18n
import redbot.cogs # Don't remove this line or core cogs won't load __all__ = ["Core"]
__all__ = ["find_spec", "Core"]
log = logging.getLogger("red") log = logging.getLogger("red")
@ -26,20 +24,6 @@ OWNER_DISCLAIMER = ("⚠ **Only** the person who is hosting Red should be "
"system.** ⚠") "system.** ⚠")
async def find_spec(bot, cog_name: str):
try:
spec = await bot.cog_mgr.find_cog(cog_name)
except RuntimeError:
real_name = ".{}".format(cog_name)
try:
mod = importlib.import_module(real_name, package='redbot.cogs')
except ImportError:
spec = None
else:
spec = mod.__spec__
return spec
_ = i18n.CogI18n("Core", __file__) _ = i18n.CogI18n("Core", __file__)
@ -50,8 +34,9 @@ class Core:
@checks.is_owner() @checks.is_owner()
async def load(self, ctx, *, cog_name: str): async def load(self, ctx, *, cog_name: str):
"""Loads a package""" """Loads a package"""
spec = await find_spec(ctx.bot, cog_name) try:
if spec is None: spec = await ctx.bot.cog_mgr.find_cog(cog_name)
except RuntimeError:
await ctx.send(_("No module by that name was found in any" await ctx.send(_("No module by that name was found in any"
" cog path.")) " cog path."))
return return
@ -83,7 +68,7 @@ class Core:
"""Reloads a package""" """Reloads a package"""
ctx.bot.unload_extension(cog_name) ctx.bot.unload_extension(cog_name)
spec = await find_spec(ctx.bot, cog_name) spec = await ctx.bot.cog_mgr.find_cog(cog_name)
if spec is None: if spec is None:
await ctx.send(_("No module by that name was found in any" await ctx.send(_("No module by that name was found in any"
" cog path.")) " cog path."))

View File

@ -1,6 +1,9 @@
import sys import sys
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, List
import hashlib
import shutil
import logging
import appdirs import appdirs
@ -10,7 +13,10 @@ if TYPE_CHECKING:
from . import Config from . import Config
__all__ = ['load_basic_configuration', 'cog_data_path', 'core_data_path', __all__ = ['load_basic_configuration', 'cog_data_path', 'core_data_path',
'storage_details', 'storage_type'] 'load_bundled_data', 'bundled_data_path', 'storage_details',
'storage_type']
log = logging.getLogger("red.data_manager")
jsonio = None jsonio = None
basic_config = None basic_config = None
@ -108,6 +114,151 @@ def core_data_path() -> Path:
return core_path.resolve() return core_path.resolve()
def _find_data_files(init_location: str) -> (Path, List[Path]):
"""
Discovers all files in the bundled data folder of an installed cog.
Parameters
----------
init_location
Returns
-------
(pathlib.Path, list of pathlib.Path)
"""
init_file = Path(init_location)
if not init_file.is_file():
return []
package_folder = init_file.parent.resolve() / 'data'
if not package_folder.is_dir():
return []
all_files = list(package_folder.rglob("*"))
return package_folder, [p.resolve()
for p in all_files
if p.is_file()]
def _compare_and_copy(to_copy: List[Path], bundled_data_dir: Path, cog_data_dir: Path):
"""
Filters out files from ``to_copy`` that already exist, and are the
same, in ``data_dir``. The files that are different are copied into
``data_dir``.
Parameters
----------
to_copy : list of pathlib.Path
bundled_data_dir : pathlib.Path
cog_data_dir : pathlib.Path
"""
def hash_bytestr_iter(bytesiter, hasher, ashexstr=False):
for block in bytesiter:
hasher.update(block)
return hasher.hexdigest() if ashexstr else hasher.digest()
def file_as_blockiter(afile, blocksize=65536):
with afile:
block = afile.read(blocksize)
while len(block) > 0:
yield block
block = afile.read(blocksize)
lookup = {p: cog_data_dir.joinpath(p.relative_to(bundled_data_dir))
for p in to_copy}
for orig, poss_existing in lookup.items():
if not poss_existing.is_file():
poss_existing.parent.mkdir(exist_ok=True, parents=True)
exists_checksum = None
else:
exists_checksum = hash_bytestr_iter(file_as_blockiter(
poss_existing.open('rb')), hashlib.sha256())
orig_checksum = ...
if exists_checksum is not None:
orig_checksum = hash_bytestr_iter(file_as_blockiter(
orig.open('rb')), hashlib.sha256())
if exists_checksum != orig_checksum:
shutil.copy(str(orig), str(poss_existing))
log.debug("Copying {} to {}".format(
orig, poss_existing
))
def load_bundled_data(cog_instance, init_location: str):
"""
This function copies (and overwrites) data from the ``data/`` folder
of the installed cog.
.. important::
This function MUST be called from the ``setup()`` function of your
cog.
Examples
--------
>>> from redbot.core import data_manager
>>>
>>> def setup(bot):
>>> cog = MyCog()
>>> data_manager.load_bundled_data(cog, __file__)
>>> bot.add_cog(cog)
Parameters
----------
cog_instance
An instance of your cog class.
init_location : str
The ``__file__`` attribute of the file where your ``setup()``
function exists.
"""
bundled_data_folder, to_copy = _find_data_files(init_location)
cog_data_folder = cog_data_path(cog_instance) / 'bundled_data'
_compare_and_copy(to_copy, bundled_data_folder, cog_data_folder)
def bundled_data_path(cog_instance) -> Path:
"""
The "data" directory that has been copied from installed cogs.
.. important::
You should *NEVER* write to this directory. Data manager will
overwrite files in this directory each time `load_bundled_data`
is called. You should instead write to the directory provided by
`cog_data_path`.
Parameters
----------
cog_instance
Returns
-------
pathlib.Path
Path object to the bundled data folder.
Raises
------
FileNotFoundError
If no bundled data folder exists or if it hasn't been loaded yet.
"""
bundled_path = cog_data_path(cog_instance) / 'bundled_data'
if not bundled_path.is_dir():
raise FileNotFoundError("No such directory {}".format(
bundled_path
))
return bundled_path
def storage_type() -> str: def storage_type() -> str:
"""Gets the storage type as a string. """Gets the storage type as a string.

View File

@ -11,7 +11,6 @@ from discord.ext import commands
from .data_manager import storage_type from .data_manager import storage_type
from .utils.chat_formatting import inline, bordered from .utils.chat_formatting import inline, bordered
from .core_commands import find_spec
from colorama import Fore, Style from colorama import Fore, Style
log = logging.getLogger("red") log = logging.getLogger("red")
@ -48,7 +47,7 @@ def init_events(bot, cli_flags):
for package in packages: for package in packages:
try: try:
spec = await find_spec(bot, package) spec = await bot.cog_mgr.find_cog(package)
bot.load_extension(spec) bot.load_extension(spec)
except Exception as e: except Exception as e:
log.exception("Failed to load package {}".format(package), log.exception("Failed to load package {}".format(package),