From 74bf2ebec1845cd0fddf119350b0817fb1346bd4 Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Tue, 21 Apr 2020 23:19:16 +0200 Subject: [PATCH] =?UTF-8?q?[bot-base]=20Ajout=20de=20la=20gestion=20des=20?= =?UTF-8?q?d=C3=A9pendances?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + Pipfile | 2 +- Pipfile.lock | 27 +------ src/bot_base/bot_base.py | 171 +++++++++++++++++++++++++-------------- src/errors.py | 2 +- src/main.py | 2 +- 6 files changed, 118 insertions(+), 87 deletions(-) diff --git a/.gitignore b/.gitignore index 86f81e7..bdd67a5 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,4 @@ data/* .env _build +/src/datas/ diff --git a/Pipfile b/Pipfile index 2587526..0c56937 100644 --- a/Pipfile +++ b/Pipfile @@ -20,4 +20,4 @@ sphinx-rtd-theme = "*" [dev-packages] [requires] -python_version = "3.7" +python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 18b8580..4b54f5d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -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": {} diff --git a/src/bot_base/bot_base.py b/src/bot_base/bot_base.py index eac8fe6..edfa08b 100644 --- a/src/bot_base/bot_base.py +++ b/src/bot_base/bot_base.py @@ -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,72 +81,114 @@ 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}.") - # Check bot_version + 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 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__.") - # 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.") - 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: - 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 + # 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']})") - self.modules.update({ - module: { - "imported": imported, - "initialized_class": main_class, - "dispatch": dispatch, - } - }) + # 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: + 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): """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) diff --git a/src/errors.py b/src/errors.py index ccbef48..e880d59 100644 --- a/src/errors.py +++ b/src/errors.py @@ -6,7 +6,7 @@ class ModuleException(BotBaseException): pass -class ModuleNotFound(ModuleException): +class ModuleNotFoundError(ModuleException): pass diff --git a/src/main.py b/src/main.py index 47ddb8a..c9564fc 100644 --- a/src/main.py +++ b/src/main.py @@ -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"))