[bot-base] Ajout de la gestion des dépendances

This commit is contained in:
Louis Chauvet 2020-04-21 23:19:16 +02:00
parent 10e1b2333c
commit 74bf2ebec1
Signed by: fomys
GPG Key ID: 1ECA046A9615ABA0
6 changed files with 118 additions and 87 deletions

1
.gitignore vendored
View File

@ -74,3 +74,4 @@ data/*
.env
_build
/src/datas/

View File

@ -20,4 +20,4 @@ sphinx-rtd-theme = "*"
[dev-packages]
[requires]
python_version = "3.7"
python_version = "3.8"

27
Pipfile.lock generated
View File

@ -1,11 +1,11 @@
{
"_meta": {
"hash": {
"sha256": "83d48f54ad14a40d4bc3b81715c5ef5bb4e604b11a7da3e9ad4db62826ff37ae"
"sha256": "d52eccc06eb777b155f756b9aadf63b98a045a979bd4882fa0ea63278245a435"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.7"
"python_version": "3.8"
},
"sources": [
{
@ -177,14 +177,6 @@
],
"version": "==1.2.0"
},
"importlib-metadata": {
"hashes": [
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
],
"markers": "python_version < '3.8'",
"version": "==1.6.0"
},
"jinja2": {
"hashes": [
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
@ -477,14 +469,6 @@
"index": "pypi",
"version": "==3.0.2"
},
"sphinx-autodoc-typehints": {
"hashes": [
"sha256:27c9e6ef4f4451766ab8d08b2d8520933b97beb21c913f3df9ab2e59b56e6c6c",
"sha256:a6b3180167479aca2c4d1ed3b5cb044a70a76cccd6b38662d39288ebd9f0dff0"
],
"index": "pypi",
"version": "==1.10.3"
},
"sphinx-rtd-theme": {
"hashes": [
"sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4",
@ -605,13 +589,6 @@
"sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2"
],
"version": "==1.4.2"
},
"zipp": {
"hashes": [
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
],
"version": "==3.1.0"
}
},
"develop": {}

View File

@ -12,7 +12,7 @@ from packaging.specifiers import SpecifierSet
from config import Config, config_types
from config.config_types import factory
from errors import IncompatibleModuleError
import errors
__version__ = "0.1.0"
MINIMAL_INFOS = ["version", "bot_version"]
@ -57,10 +57,21 @@ class BotBase(discord.Client):
})
self.config.load()
self.load_module("test_module")
async def on_ready(self):
self.info("Bot ready.")
try:
self.load_modules()
except errors.ModuleException as e:
self.loop.stop()
raise e
def load_modules(self):
self.info("Load modules...")
for module in self.config["modules"]:
if module not in self.modules.keys():
self.load_module(module)
self.info("Modules loaded.")
def load_module(self, module: str) -> None:
"""
@ -70,31 +81,54 @@ class BotBase(discord.Client):
:raise IncompatibleModuleError: If module is incompatible
:param str module: module to load
"""
self.info(f"Attempt to load module {module}...")
# Check if module exists
if not os.path.isdir(os.path.join(self.config["modules_folder"], module)):
raise ModuleNotFoundError(f"Module {module} not found in modules folder ({self.config['modules_folder']}.)")
self.warning(f"Attempt to load unknown module {module}.")
raise errors.ModuleNotFoundError(f"Module {module} not found in modules folder ({self.config['modules_folder']}.)")
if not os.path.isfile(os.path.join(self.config["modules_folder"], module, "infos.toml")):
raise IncompatibleModuleError(f"Module {module} is incompatible: no infos.toml found.")
self.warning(f"Attempt to load incompatible module {module}: no infos.toml found")
raise errors.IncompatibleModuleError(f"Module {module} is incompatible: no infos.toml found.")
# Check infos.toml integrity
with open(os.path.join(self.config["modules_folder"], module, "infos.toml")) as f:
infos = toml.load(f)
for key in MINIMAL_INFOS:
if key not in infos.keys():
raise IncompatibleModuleError(f"Missing information for module {module}: missing {key}.")
self.warning(f"Attempt to load incompatible module {module}: missing information {key}")
raise errors.IncompatibleModuleError(f"Missing information for module {module}: missing {key}.")
# Check bot_version
bot_version_specifier = SpecifierSet(infos["bot_version"])
if __version__ not in bot_version_specifier:
raise IncompatibleModuleError(f"Module {module} is not compatible with your current bot version "
self.warning(f"Attempt to load incompatible module {module}: need bot version {infos['bot_version']} "
f"and you have {__version__}")
raise errors.IncompatibleModuleError(f"Module {module} is not compatible with your current bot version "
f"(need {infos['bot_version']} and you have {__version__}).")
# Check dependencies
if infos.get("dependencies"):
for dep, version in infos["dependencies"].items():
if not dep in self.modules.keys():
self.load_module(dep)
dep_version_specifier = SpecifierSet(version)
if self.modules[dep]["infos"]["version"] not in dep_version_specifier:
self.warning(f"Attempt to load incompatible module {module}: (require {dep} ({version}) "
f"and you have {dep} ({self.modules[dep]['infos']['version']})")
raise errors.IncompatibleModuleError(f"Module {module} is not compatible with your current install "
f"(require {dep} ({version}) and you have {dep} "
f"({self.modules[dep]['infos']['version']})")
# Check if module is meta
if infos.get("metamodule", False) == False:
# Check if module have __main_class__
imported = importlib.import_module(module)
try:
main_class = imported.__main_class__
except AttributeError:
raise IncompatibleModuleError(f"Module {module} does not provide __main_class__.")
self.warning(f"Attempt to load incompatible module {module}: no __main_class__ found")
raise errors.IncompatibleModuleError(f"Module {module} does not provide __main_class__.")
# Check if __main_class__ is a class
if not inspect.isclass(main_class):
raise IncompatibleModuleError(f"Module {module} contains __main_class__ but it is not a type.")
self.warning(f"Attempt to load incompatible module {module}: __main_class__ is not a type")
raise errors.IncompatibleModuleError(f"Module {module} contains __main_class__ but it is not a type.")
try:
main_class = main_class(self)
except TypeError:
@ -104,11 +138,14 @@ class BotBase(discord.Client):
try:
dispatch = main_class.__dispatch__
except AttributeError:
raise IncompatibleModuleError(f"Module {module} mainclass ({main_class}) does not provide __dispatch__"
self.warning(f"Attempt to load incompatible module {module}: __dispatch_ not found")
raise errors.IncompatibleModuleError(f"Module {module} mainclass ({main_class}) does not provide __dispatch__"
f" attribute)")
# Check if __dispatch__ is function
if not inspect.isfunction(imported.__main_class__.__dispatch__):
raise IncompatibleModuleError(f"Module {module} mainclass ({main_class}) provides __dispatch__, but it is "
self.warning(f"Attempt to load incompatible module {module}: __dispatch__ is not a function")
raise errors.IncompatibleModuleError(
f"Module {module} mainclass ({main_class}) provides __dispatch__, but it is "
f"not a function ({dispatch}).")
# Check if __dispatch__ can have variable positional and keyword aguments (to avoid future error on each event)
sig = inspect.signature(dispatch)
@ -119,23 +156,39 @@ class BotBase(discord.Client):
elif p.kind == p.VAR_KEYWORD:
kwargs_present = True
if not args_present:
raise IncompatibleModuleError(
self.warning(f"Attempt to load incompatible module {module}: __dispatch__ doesn't accept variable "
f"positional arguments")
raise errors.IncompatibleModuleError(
f"Module {module} mainclass ({main_class}) provide __dispatch__ function, but "
f"this function doesn't accept variable positionnal arguments.")
f"this function doesn't accept variable positional arguments.")
if not kwargs_present:
raise IncompatibleModuleError(
self.warning(f"Attempt to load incompatible module {module}: __dispatch__ doesn't accept variable "
f"keywords arguments.")
raise errors.IncompatibleModuleError(
f"Module {module} mainclass ({main_class}) provide __dispatch__ function, but "
f"this function doesn't accept variable keywords arguments.")
# Module is compatible!
# Add module to loaded modules
self.info(f"Add modules {module} to current modules.")
self.modules.update({
module: {
"infos": infos,
"imported": imported,
"initialized_class": main_class,
"dispatch": dispatch,
}
})
else: # Module is metamodule
self.info(f"Add modules {module} to current modules")
self.modules.update({
module: {
"infos": infos,
"dispatch": lambda *x, **y: None
}
})
if module not in self.config["modules"]:
self.config.set({"modules": self.config["modules"] + [module]})
self.config.save()
def dispatch(self, event, *args, **kwargs):
"""Dispatch event"""
@ -147,14 +200,14 @@ class BotBase(discord.Client):
def info(self, *args, **kwargs):
if self.log:
self.log.info(*args, **kwargs)
self.dispatch("on_log_info", *args, **kwargs)
self.dispatch("log_info", *args, **kwargs)
def error(self, *args, **kwargs):
if self.log:
self.log.error(*args, **kwargs)
self.dispatch("on_log_error", *args, **kwargs)
self.dispatch("log_error", *args, **kwargs)
def warning(self, *args, **kwargs):
if self.log:
self.log.warning(*args, **kwargs)
self.dispatch("on_log_warning", *args, **kwargs)
self.dispatch("log_warning", *args, **kwargs)

View File

@ -6,7 +6,7 @@ class ModuleException(BotBaseException):
pass
class ModuleNotFound(ModuleException):
class ModuleNotFoundError(ModuleException):
pass

View File

@ -25,7 +25,7 @@ def setup_logging(default_path='data/log_config.json', default_level=logging.INF
setup_logging()
if __name__ == "__main__":
client = BotBase(max_messages=500000)
client = BotBase(max_messages=500000, data_folder="datas", modules_folder=os.environ.get("LOCAL_MODULES", "modules"))
async def start_bot():
await client.start(os.environ.get("DISCORD_TOKEN"))