[bot-base] Ajout de la gestion des dépendances
This commit is contained in:
parent
10e1b2333c
commit
74bf2ebec1
1
.gitignore
vendored
1
.gitignore
vendored
@ -74,3 +74,4 @@ data/*
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
_build
|
_build
|
||||||
|
/src/datas/
|
||||||
|
2
Pipfile
2
Pipfile
@ -20,4 +20,4 @@ sphinx-rtd-theme = "*"
|
|||||||
[dev-packages]
|
[dev-packages]
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.7"
|
python_version = "3.8"
|
||||||
|
27
Pipfile.lock
generated
27
Pipfile.lock
generated
@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "83d48f54ad14a40d4bc3b81715c5ef5bb4e604b11a7da3e9ad4db62826ff37ae"
|
"sha256": "d52eccc06eb777b155f756b9aadf63b98a045a979bd4882fa0ea63278245a435"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
"python_version": "3.7"
|
"python_version": "3.8"
|
||||||
},
|
},
|
||||||
"sources": [
|
"sources": [
|
||||||
{
|
{
|
||||||
@ -177,14 +177,6 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.2.0"
|
"version": "==1.2.0"
|
||||||
},
|
},
|
||||||
"importlib-metadata": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
|
|
||||||
"sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
|
|
||||||
],
|
|
||||||
"markers": "python_version < '3.8'",
|
|
||||||
"version": "==1.6.0"
|
|
||||||
},
|
|
||||||
"jinja2": {
|
"jinja2": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
"sha256:89aab215427ef59c34ad58735269eb58b1a5808103067f7bb9d5836c651b3bb0",
|
||||||
@ -477,14 +469,6 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.0.2"
|
"version": "==3.0.2"
|
||||||
},
|
},
|
||||||
"sphinx-autodoc-typehints": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:27c9e6ef4f4451766ab8d08b2d8520933b97beb21c913f3df9ab2e59b56e6c6c",
|
|
||||||
"sha256:a6b3180167479aca2c4d1ed3b5cb044a70a76cccd6b38662d39288ebd9f0dff0"
|
|
||||||
],
|
|
||||||
"index": "pypi",
|
|
||||||
"version": "==1.10.3"
|
|
||||||
},
|
|
||||||
"sphinx-rtd-theme": {
|
"sphinx-rtd-theme": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4",
|
"sha256:00cf895504a7895ee433807c62094cf1e95f065843bf3acd17037c3e9a2becd4",
|
||||||
@ -605,13 +589,6 @@
|
|||||||
"sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2"
|
"sha256:e15199cdb423316e15f108f51249e44eb156ae5dba232cb73be555324a1d49c2"
|
||||||
],
|
],
|
||||||
"version": "==1.4.2"
|
"version": "==1.4.2"
|
||||||
},
|
|
||||||
"zipp": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
|
|
||||||
"sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
|
|
||||||
],
|
|
||||||
"version": "==3.1.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {}
|
"develop": {}
|
||||||
|
@ -12,7 +12,7 @@ from packaging.specifiers import SpecifierSet
|
|||||||
|
|
||||||
from config import Config, config_types
|
from config import Config, config_types
|
||||||
from config.config_types import factory
|
from config.config_types import factory
|
||||||
from errors import IncompatibleModuleError
|
import errors
|
||||||
|
|
||||||
__version__ = "0.1.0"
|
__version__ = "0.1.0"
|
||||||
MINIMAL_INFOS = ["version", "bot_version"]
|
MINIMAL_INFOS = ["version", "bot_version"]
|
||||||
@ -57,10 +57,21 @@ class BotBase(discord.Client):
|
|||||||
})
|
})
|
||||||
|
|
||||||
self.config.load()
|
self.config.load()
|
||||||
self.load_module("test_module")
|
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
self.info("Bot ready.")
|
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:
|
def load_module(self, module: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -70,72 +81,114 @@ class BotBase(discord.Client):
|
|||||||
:raise IncompatibleModuleError: If module is incompatible
|
:raise IncompatibleModuleError: If module is incompatible
|
||||||
:param str module: module to load
|
:param str module: module to load
|
||||||
"""
|
"""
|
||||||
|
self.info(f"Attempt to load module {module}...")
|
||||||
# Check if module exists
|
# Check if module exists
|
||||||
if not os.path.isdir(os.path.join(self.config["modules_folder"], module)):
|
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")):
|
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
|
# Check infos.toml integrity
|
||||||
with open(os.path.join(self.config["modules_folder"], module, "infos.toml")) as f:
|
with open(os.path.join(self.config["modules_folder"], module, "infos.toml")) as f:
|
||||||
infos = toml.load(f)
|
infos = toml.load(f)
|
||||||
for key in MINIMAL_INFOS:
|
for key in MINIMAL_INFOS:
|
||||||
if key not in infos.keys():
|
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}")
|
||||||
# Check bot_version
|
raise errors.IncompatibleModuleError(f"Missing information for module {module}: missing {key}.")
|
||||||
|
# Check bot_version
|
||||||
bot_version_specifier = SpecifierSet(infos["bot_version"])
|
bot_version_specifier = SpecifierSet(infos["bot_version"])
|
||||||
if __version__ not in bot_version_specifier:
|
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__}).")
|
f"(need {infos['bot_version']} and you have {__version__}).")
|
||||||
# Check if module have __main_class__
|
# Check dependencies
|
||||||
imported = importlib.import_module(module)
|
if infos.get("dependencies"):
|
||||||
try:
|
for dep, version in infos["dependencies"].items():
|
||||||
main_class = imported.__main_class__
|
if not dep in self.modules.keys():
|
||||||
except AttributeError:
|
self.load_module(dep)
|
||||||
raise IncompatibleModuleError(f"Module {module} does not provide __main_class__.")
|
dep_version_specifier = SpecifierSet(version)
|
||||||
# Check if __main_class__ is a class
|
if self.modules[dep]["infos"]["version"] not in dep_version_specifier:
|
||||||
if not inspect.isclass(main_class):
|
self.warning(f"Attempt to load incompatible module {module}: (require {dep} ({version}) "
|
||||||
raise IncompatibleModuleError(f"Module {module} contains __main_class__ but it is not a type.")
|
f"and you have {dep} ({self.modules[dep]['infos']['version']})")
|
||||||
try:
|
raise errors.IncompatibleModuleError(f"Module {module} is not compatible with your current install "
|
||||||
main_class = main_class(self)
|
f"(require {dep} ({version}) and you have {dep} "
|
||||||
except TypeError:
|
f"({self.modules[dep]['infos']['version']})")
|
||||||
# Module don't need client reference
|
|
||||||
main_class = main_class()
|
|
||||||
# Check if __main_class__ have __dispatch__ attribute
|
|
||||||
try:
|
|
||||||
dispatch = main_class.__dispatch__
|
|
||||||
except AttributeError:
|
|
||||||
raise 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 "
|
|
||||||
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)
|
|
||||||
args_present, kwargs_present = False, False
|
|
||||||
for p in sig.parameters.values():
|
|
||||||
if p.kind == p.VAR_POSITIONAL:
|
|
||||||
args_present = True
|
|
||||||
elif p.kind == p.VAR_KEYWORD:
|
|
||||||
kwargs_present = True
|
|
||||||
if not args_present:
|
|
||||||
raise IncompatibleModuleError(
|
|
||||||
f"Module {module} mainclass ({main_class}) provide __dispatch__ function, but "
|
|
||||||
f"this function doesn't accept variable positionnal arguments.")
|
|
||||||
if not kwargs_present:
|
|
||||||
raise 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.modules.update({
|
# Check if module is meta
|
||||||
module: {
|
if infos.get("metamodule", False) == False:
|
||||||
"imported": imported,
|
# Check if module have __main_class__
|
||||||
"initialized_class": main_class,
|
imported = importlib.import_module(module)
|
||||||
"dispatch": dispatch,
|
try:
|
||||||
}
|
main_class = imported.__main_class__
|
||||||
})
|
except AttributeError:
|
||||||
|
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):
|
||||||
|
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:
|
||||||
|
# Module don't need client reference
|
||||||
|
main_class = main_class()
|
||||||
|
# Check if __main_class__ have __dispatch__ attribute
|
||||||
|
try:
|
||||||
|
dispatch = main_class.__dispatch__
|
||||||
|
except AttributeError:
|
||||||
|
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__):
|
||||||
|
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)
|
||||||
|
args_present, kwargs_present = False, False
|
||||||
|
for p in sig.parameters.values():
|
||||||
|
if p.kind == p.VAR_POSITIONAL:
|
||||||
|
args_present = True
|
||||||
|
elif p.kind == p.VAR_KEYWORD:
|
||||||
|
kwargs_present = True
|
||||||
|
if not args_present:
|
||||||
|
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 positional arguments.")
|
||||||
|
if not kwargs_present:
|
||||||
|
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):
|
def dispatch(self, event, *args, **kwargs):
|
||||||
"""Dispatch event"""
|
"""Dispatch event"""
|
||||||
@ -147,14 +200,14 @@ class BotBase(discord.Client):
|
|||||||
def info(self, *args, **kwargs):
|
def info(self, *args, **kwargs):
|
||||||
if self.log:
|
if self.log:
|
||||||
self.log.info(*args, **kwargs)
|
self.log.info(*args, **kwargs)
|
||||||
self.dispatch("on_log_info", *args, **kwargs)
|
self.dispatch("log_info", *args, **kwargs)
|
||||||
|
|
||||||
def error(self, *args, **kwargs):
|
def error(self, *args, **kwargs):
|
||||||
if self.log:
|
if self.log:
|
||||||
self.log.error(*args, **kwargs)
|
self.log.error(*args, **kwargs)
|
||||||
self.dispatch("on_log_error", *args, **kwargs)
|
self.dispatch("log_error", *args, **kwargs)
|
||||||
|
|
||||||
def warning(self, *args, **kwargs):
|
def warning(self, *args, **kwargs):
|
||||||
if self.log:
|
if self.log:
|
||||||
self.log.warning(*args, **kwargs)
|
self.log.warning(*args, **kwargs)
|
||||||
self.dispatch("on_log_warning", *args, **kwargs)
|
self.dispatch("log_warning", *args, **kwargs)
|
||||||
|
@ -6,7 +6,7 @@ class ModuleException(BotBaseException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ModuleNotFound(ModuleException):
|
class ModuleNotFoundError(ModuleException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ def setup_logging(default_path='data/log_config.json', default_level=logging.INF
|
|||||||
setup_logging()
|
setup_logging()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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():
|
async def start_bot():
|
||||||
await client.start(os.environ.get("DISCORD_TOKEN"))
|
await client.start(os.environ.get("DISCORD_TOKEN"))
|
||||||
|
Loading…
Reference in New Issue
Block a user