From 903d43efb34d72a67f9ebd3d3a65127200712f41 Mon Sep 17 00:00:00 2001 From: Louis Chauvet Date: Tue, 14 Apr 2020 02:31:36 +0200 Subject: [PATCH] =?UTF-8?q?[bot-base]=20Ajout=20de=20la=20doc=20[config]?= =?UTF-8?q?=20D=C3=A9but=20de=20la=20r=C3=A9=C3=A9criture,=20pour=20l'inst?= =?UTF-8?q?ant=20ca=20casse=20tous=20les=20modules,=20je=20r=C3=A9pare=20d?= =?UTF-8?q?emain,=20=C3=A9criture=20d'une=20partie=20de=20la=20doc=20[util?= =?UTF-8?q?s/emojis]=20Passage=20=C3=A0=20la=20notation=20unicode,=20j'en?= =?UTF-8?q?=20ai=20marre=20des=20doubles=20caract=C3=A8res,=20=C3=A9critur?= =?UTF-8?q?e=20de=20la=20doc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Makefile | 20 ++ config/Base.py | 88 --------- config/FileSystem.py | 27 --- config/__init__.py | 5 +- config/base.py | 128 +++++++++++++ config/config_types/__init__.py | 32 ++++ config/config_types/base_type.py | 20 ++ config/config_types/dict.py | 63 +++++++ config/config_types/discord_types/__init__.py | 6 + config/config_types/discord_types/channel.py | 36 ++++ config/config_types/discord_types/guild.py | 36 ++++ config/config_types/discord_types/role.py | 36 ++++ config/config_types/discord_types/user.py | 36 ++++ config/config_types/float.py | 38 ++++ config/config_types/int.py | 177 ++++++++++++++++++ config/config_types/list.py | 42 +++++ config/config_types/str.py | 35 ++++ config/log_config.json | 54 ++++++ main.py | 66 ++++--- make.bat | 35 ++++ modules/avalon/__init__.py | 6 +- modules/base/Base.py | 4 +- modules/errors/__init__.py | 4 +- modules/newmember/__init__.py | 2 +- modules/perdu/__init__.py | 5 +- modules/readrules/__init__.py | 2 +- modules/roles/__init__.py | 2 +- modules/rtfgd/__init__.py | 2 +- pytest.ini | 2 + .../api/config.config_types.discord_types.rst | 10 + source/api/config.config_types.rst | 20 ++ source/api/config.rst | 19 ++ source/api/modules.rst | 8 + source/api/utils.emojis.rst | 10 + source/api/utils.rst | 19 ++ source/conf.py | 59 ++++++ source/index.rst | 34 ++++ source/module_creation/index.rst | 8 + source/module_creation/intro.rst | 67 +++++++ utils/emojis.py | 36 +++- 40 files changed, 1139 insertions(+), 160 deletions(-) create mode 100644 Makefile delete mode 100644 config/Base.py delete mode 100644 config/FileSystem.py create mode 100644 config/base.py create mode 100644 config/config_types/__init__.py create mode 100644 config/config_types/base_type.py create mode 100644 config/config_types/dict.py create mode 100644 config/config_types/discord_types/__init__.py create mode 100644 config/config_types/discord_types/channel.py create mode 100644 config/config_types/discord_types/guild.py create mode 100644 config/config_types/discord_types/role.py create mode 100644 config/config_types/discord_types/user.py create mode 100644 config/config_types/float.py create mode 100644 config/config_types/int.py create mode 100644 config/config_types/list.py create mode 100644 config/config_types/str.py create mode 100644 config/log_config.json create mode 100644 make.bat create mode 100644 pytest.ini create mode 100644 source/api/config.config_types.discord_types.rst create mode 100644 source/api/config.config_types.rst create mode 100644 source/api/config.rst create mode 100644 source/api/modules.rst create mode 100644 source/api/utils.emojis.rst create mode 100644 source/api/utils.rst create mode 100644 source/conf.py create mode 100644 source/index.rst create mode 100644 source/module_creation/index.rst create mode 100644 source/module_creation/intro.rst diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/config/Base.py b/config/Base.py deleted file mode 100644 index 958d9aa..0000000 --- a/config/Base.py +++ /dev/null @@ -1,88 +0,0 @@ -from __future__ import annotations - -from typing import Dict, Any, Optional - - -class Config: - name: Optional[str] - parent: Optional[Config] - config: Dict[Any, Any] - - def __init__(self, parent: Config = None, name: str = None): - """Create Config - - :param parent: Parent configuration - :param name: Configuration name - :type parent: Config - :type name: str""" - self.parent = parent - self.config = dict() - self.name = None - if self.parent: - self.name = name - - def init(self, config): - """Load default configuration - - :param config: Default configuration - :type config: dict - :return: None""" - # Load data from config file before initialisation - self.load() - # Get data from parent - if self.parent is not None: - self.parent.config[self.name] = self.parent.config.get(self.name) if self.parent.config.get( - self.name) is not None else self.config - self.config = self.parent.config[self.name] - # Set config only if not already defined - for k, v in config.items(): - self.config[k] = self.config.get(k) if self.config.get(k) is not None else v - # Save new datas - self.save() - - def _save(self): - """Internal function for save - - Must be overridden by all type of config to handle saving""" - # Call parent save if necessary - if self.parent: - self.parent.save() - - def save(self): - """Public save function - - Do not override""" - self._save() - - def _load(self): - """Internal function for load - - Mus be overridden by all type of config to handle loading""" - # Load parent if necessary - if self.parent: - self.parent.load() - self.config = self.parent.config.get(self.name) - # Initialize parent if necessary - if self.config is None: - self.parent.config[self.name] = {} - self.config = {} - - def load(self): - """Public load function - - Do not override""" - self._load() - - def __getattr__(self, item): - return self.config.get(item) - - def __getitem__(self, item): - return self.config.get(item) - - def __setitem__(self, key, value): - if self.parent: - self.parent[self.name][key] = value - self.config = self.parent[self.name] - else: - self.config[key] = value - self.save() diff --git a/config/FileSystem.py b/config/FileSystem.py deleted file mode 100644 index e08e0dd..0000000 --- a/config/FileSystem.py +++ /dev/null @@ -1,27 +0,0 @@ -import os - -import toml - -from config.Base import Config - - -class FSConfig(Config): - path: str - - def __init__(self, path="config.toml", *args, **kwargs): - super().__init__(*args, **kwargs) - self.path = path - os.makedirs(os.path.dirname(path), exist_ok=True) - open(path, "a").close() - - def _load(self): - with open(self.path, "r") as file: - content = file.read() - self.config = toml.loads(content) - if self.config is None: - self.config = {} - - def _save(self): - content = toml.dumps(self.config) - with open(self.path, "w") as file: - file.write(content) diff --git a/config/__init__.py b/config/__init__.py index 54f2c6d..11f8bdc 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1,2 +1,3 @@ -from .Base import Config -from .FileSystem import FSConfig \ No newline at end of file +from .base import Config + +__all__ = ["Config"] diff --git a/config/base.py b/config/base.py new file mode 100644 index 0000000..a17af81 --- /dev/null +++ b/config/base.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +from typing import Dict, Type, Any, TYPE_CHECKING + +import toml + +if TYPE_CHECKING: + from config.config_types.base_type import BaseType + + +class Config: + #: Path of config file + path: str + #: Current fields + fields: Dict[str, BaseType] + + def __init__(self, path: str) -> None: + """ + Create config object + + Basic usage: + + >>> config = Config("doctest_config.toml") + + :param path: Path of config file + :type path: str + :rtype: None + :rtype: None + """ + self.fields = {} + self.path = path + + def register(self, name: str, type_: Type[BaseType]) -> None: + """ + Register option + + Basic usage: + + >>> from config.config_types import factory, Int + >>> config = Config("doctest_config.toml") + >>> config.register("my_parameter", factory(Int)) + + :param name: Name of config parameter + :param type_: Type of config parameter + :type name: str + :type type_: Type[BaseType] + :return: None + :rtype: None + """ + self.fields.update({ + name: type_() + }) + + def set(self, values: dict) -> None: + """ + Set all parameters with values (and override old ones) + + Basic usage: + + >>> from config.config_types import factory, Int + >>> config = Config("doctest_config.toml") + >>> config.register("my_parameter", factory(Int)) + >>> config.set({"my_parameter": 3}) + + :type values: dict + :param values: dict of parameters + :return: None + :rtype: None + """ + for k, v in values.items(): + self.fields[k].set(v) + + def save(self) -> None: + """ + Save config to ``self.file`` + + Basic usage: + + >>> from config.config_types import factory, Int + >>> config = Config("doctest_config.toml") + >>> config.register("my_parameter", factory(Int)) + >>> config.set({"my_parameter": 3}) + >>> config.save() + + :return: None + """ + with open(self.path, 'w') as file: + toml.dump({k: v.to_save() for k, v in self.fields.items()}, file) + + def load(self) -> None: + """ + Load config from ``self.file`` + + Basic usage: + + >>> from config.config_types import factory, Int + >>> config = Config("doctest_config.toml") + >>> config.register("my_parameter", factory(Int)) + >>> config.set({"my_parameter": 3}) + >>> config.save() + >>> new_config = Config("doctest_config.toml") + >>> new_config.register("my_parameter", factory(Int)) + >>> new_config.load() + >>> new_config["my_parameter"] + 3 + + + :return: None + """ + with open(self.path, 'r') as file: + self.set(toml.load(file)) + + def __getitem__(self, item: str) -> Any: + """ + Save config to ``self.file`` + + Basic usage: + + >>> from config.config_types import factory, Int + >>> config = Config("doctest_config.toml") + >>> config.register("my_parameter", factory(Int)) + >>> config.set({"my_parameter": 3}) + >>> print(config["my_parameter"]) + 3 + + :return: None + """ + return self.fields[item].get() diff --git a/config/config_types/__init__.py b/config/config_types/__init__.py new file mode 100644 index 0000000..35cdd0d --- /dev/null +++ b/config/config_types/__init__.py @@ -0,0 +1,32 @@ +from typing import Type + +from config.config_types.base_type import BaseType +from config.config_types.dict import Dict +from config.config_types.float import Float +from config.config_types.int import Int +from config.config_types.list import List +from config.config_types.str import Str + +__all__ = ['factory', "BaseType", 'Dict', 'Float', 'Int', 'List', 'Str'] + + +def factory(type: Type[BaseType], *args, **kwargs): + """ + Create a new test ``type`` with parameters args and kwargs + + :Basic usage: + + >>> factory(Int) # doctest: +ELLIPSIS + + >>> factory(Int, min=0, max=10) # doctest: +ELLIPSIS + + + :param type: Type to create + :return: New type + """ + + class Type(type): + def __init__(self): + super().__init__(*args, **kwargs) + + return Type diff --git a/config/config_types/base_type.py b/config/config_types/base_type.py new file mode 100644 index 0000000..c312f02 --- /dev/null +++ b/config/config_types/base_type.py @@ -0,0 +1,20 @@ +class BaseType: + def check_value(self, value): + """Check if value is good""" + pass + + def set(self, value): + """Check and set value""" + pass + + def get(self): + """Get value""" + pass + + def to_save(self): + """Build a serializable data to save""" + pass + + def load(self, value): + """Fill with value""" + pass diff --git a/config/config_types/dict.py b/config/config_types/dict.py new file mode 100644 index 0000000..5d6ed3f --- /dev/null +++ b/config/config_types/dict.py @@ -0,0 +1,63 @@ +from typing import Type + +from config.config_types.base_type import BaseType + + +class Dict(BaseType): + type_key: Type[BaseType] + type_value: Type[BaseType] + + def __init__(self, type_key, type_value): + self.type_key = type_key + self.type_value = type_value + self.values = None + + def check_value(self, value): + """Check if value is good""" + o_key = self.type_key() + o_value = self.type_value() + if type(value) == dict: + for k, v in value.items(): + if not (o_key.check_value(k) and o_value.check_value(v)): + return False + return True + if (type(value) == list or type(value) == tuple) and len(value) == 2: + return o_key.check_value(value[0]) and o_value.check_value(value[1]) + return False + + def set(self, value): + """Check and set value""" + new_dict = dict() + if not self.check_value(value): + raise ValueError("Tentative de définir une valeur incompatible") + for k, v in value.items(): + new_key = self.type_key() + new_key.set(k) + new_value = self.type_value() + new_value.set(v) + new_dict.update({new_key: new_value}) + self.values = new_dict + + def get(self): + """Get value""" + if self.values is not None: + return {k.get(): v.get() for k, v in self.values.items()} + return dict() + + def to_save(self): + """Build a serializable data to save""" + # Construction d'une liste de liste: [[key, value], ...] + if self.values is not None: + return [[k.to_save(), v.to_save()] for k, v in self.values.items()] + return list() + + def load(self, value): + """Fill with value""" + new_values = dict() + for v in value: + new_key = self.type_key() + new_key.load(v[0]) + new_value = self.type_value() + new_value.load(v[1]) + new_values.update({new_key: new_value}) + self.values = new_values diff --git a/config/config_types/discord_types/__init__.py b/config/config_types/discord_types/__init__.py new file mode 100644 index 0000000..0f66960 --- /dev/null +++ b/config/config_types/discord_types/__init__.py @@ -0,0 +1,6 @@ +from config.config_types.discord_types.channel import Channel +from config.config_types.discord_types.guild import Guild +from config.config_types.discord_types.user import User +from config.config_types.discord_types.role import Role + +__all__ = ['Channel', "Guild", "User", "Role"] \ No newline at end of file diff --git a/config/config_types/discord_types/channel.py b/config/config_types/discord_types/channel.py new file mode 100644 index 0000000..edffe0c --- /dev/null +++ b/config/config_types/discord_types/channel.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from config.config_types.base_type import BaseType + +if TYPE_CHECKING: + from main import LBI + + +class Channel(BaseType): + client: LBI + + def __init__(self, client): + self.value = None + self.client = client + + def check_value(self, value): + return True + + def set(self, value): + if self.check_value(value): + self.value = value + return + raise ValueError("Tentative de définir une valeur incompatible") + + def get(self): + return self.value + + def to_save(self): + return self.value + + def load(self, value): + if self.check_value(value): + raise ValueError("Tentative de charger une donnée incompatible.") + self.value = value diff --git a/config/config_types/discord_types/guild.py b/config/config_types/discord_types/guild.py new file mode 100644 index 0000000..161491e --- /dev/null +++ b/config/config_types/discord_types/guild.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from config.config_types.base_type import BaseType + +if TYPE_CHECKING: + from main import LBI + + +class Guild(BaseType): + client: LBI + + def __init__(self, client): + self.value = None + self.client = client + + def check_value(self, value): + return True + + def set(self, value): + if self.check_value(value): + self.value = value + return + raise ValueError("Tentative de définir une valeur incompatible") + + def get(self): + return self.value + + def to_save(self): + return self.value + + def load(self, value): + if self.check_value(value): + raise ValueError("Tentative de charger une donnée incompatible.") + self.value = value diff --git a/config/config_types/discord_types/role.py b/config/config_types/discord_types/role.py new file mode 100644 index 0000000..3d600fb --- /dev/null +++ b/config/config_types/discord_types/role.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from config.config_types.base_type import BaseType + +if TYPE_CHECKING: + from main import LBI + + +class Role(BaseType): + client: LBI + + def __init__(self, client): + self.value = None + self.client = client + + def check_value(self, value): + return True + + def set(self, value): + if self.check_value(value): + self.value = value + return + raise ValueError("Tentative de définir une valeur incompatible") + + def get(self): + return self.value + + def to_save(self): + return self.value + + def load(self, value): + if self.check_value(value): + raise ValueError("Tentative de charger une donnée incompatible.") + self.value = value diff --git a/config/config_types/discord_types/user.py b/config/config_types/discord_types/user.py new file mode 100644 index 0000000..2c53a87 --- /dev/null +++ b/config/config_types/discord_types/user.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from config.config_types.base_type import BaseType + +if TYPE_CHECKING: + from main import LBI + +class User(BaseType): + + client: LBI + + def __init__(self, client): + self.value = None + self.client = client + + def check_value(self, value): + return True + + def set(self, value): + if self.check_value(value): + self.value = value + return + raise ValueError("Tentative de définir une valeur incompatible") + + def get(self): + return self.value + + def to_save(self): + return self.value + + def load(self, value): + if self.check_value(value): + raise ValueError("Tentative de charger une donnée incompatible.") + self.value = value \ No newline at end of file diff --git a/config/config_types/float.py b/config/config_types/float.py new file mode 100644 index 0000000..0020bdd --- /dev/null +++ b/config/config_types/float.py @@ -0,0 +1,38 @@ +from config.config_types.base_type import BaseType + + +class Float(BaseType): + def __init__(self, min_=None, max_=None): + self.value = None + self.min = min_ + self.max = max_ + + def check_value(self, value): + try: + float(value) + except ValueError: + return False + # TODO: < ou <=? > ou >=? + # Check min/max + if self.min is not None and float(value) < self.min: + return False + if self.max is not None and float(value) > self.max: + return False + return True + + def set(self, value): + if self.check_value(value): + self.value = value + return + raise ValueError("Tentative de définir une valeur incompatible") + + def get(self): + return self.value + + def to_save(self): + return self.value + + def load(self, value): + if self.check_value(value): + raise ValueError("Tentative de charger une donnée incompatible.") + self.value = value diff --git a/config/config_types/int.py b/config/config_types/int.py new file mode 100644 index 0000000..7e18206 --- /dev/null +++ b/config/config_types/int.py @@ -0,0 +1,177 @@ +from typing import Optional, List + +from config.config_types.base_type import BaseType + + +class Int(BaseType): + #: Max value for parameter + max: Optional[int] + #: Min value for parameter + min: Optional[int] + #: List of valid values for parameter + values: Optional[List[int]] + #: Current value of parameter + value: Optional[int] + + def __init__(self, min: Optional[int] = None, max: Optional[int] = None, + values: Optional[List[int]] = None) -> None: + """ + Base Int type for config + + :Basic usage: + + >>> Int() + + >>> Int(min=0) + + >>> Int(max=0) + + >>> Int(min=10, max=20) + + >>> Int(values=[2, 3, 5, 7]) + + >>> Int(min=0, values=[3, 4, 5]) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValueError: ... + + :raise ValueError: If min and/or max are set when using values + :param min: Min value for this parameter + :param max: Max value for this parameter + :param values: This parameter can only be in one of these values (raise ValueError if min or max are set with values) + """ + self.value = None + if values is not None and (min is not None or max is not None): + raise ValueError("Il n'est pas possible de définir un champ avec à " + "la fois un max/min et une série de valeur") + self.values = values + self.min = min + self.max = max + + def check_value(self, value: int) -> bool: + """ + Check if value is a correct int + + Check if value is int, and if applicable, between ``min`` and ``max`` or in ``values`` + + :Basic usage: + + >>> positive = Int(min=0) + >>> negative = Int(max=0) + >>> ten_to_twenty = Int(min=10, max=20) + >>> prime = Int(values=[2,3,5,7]) + >>> positive.check_value(0) + True + >>> positive.check_value(-2) + False + >>> positive.check_value(345) + True + >>> negative.check_value(0) + True + >>> negative.check_value(-2) + True + >>> negative.check_value(345) + False + >>> ten_to_twenty.check_value(10) + True + >>> ten_to_twenty.check_value(-2) + False + >>> ten_to_twenty.check_value(20) + True + >>> prime.check_value(2) + True + >>> prime.check_value(4) + False + >>> prime.check_value(5) + True + + :param value: value to check + :return: True if value is correct + """ + try: + int(value) + except ValueError: + return False + # TODO: < ou <=? > ou >=? + # Check min/max + if self.min is not None and int(value) < self.min: + return False + if self.max is not None and int(value) > self.max: + return False + # Check validity + if self.values is not None and value not in self.values: + return False + return True + + def set(self, value: int) -> None: + """ + Set value of parameter + + :Basic usage: + + >>> my_int = Int(min=0) + >>> my_int.set(34) + >>> my_int.set(-34) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValueError: ... + + :raise ValueError: if attempt to set invalid value + :param value: Value to set + :return: None + """ + if not self.check_value(value): + raise ValueError("Tentative de définir une valeur incompatible") + self.value = value + + def get(self) -> Optional[int]: + """ + Get value of parameter + + :Basic usage: + + >>> my_int = Int() + >>> my_int.set(34) + >>> my_int.get() + 34 + + :return: Value of parameter + """ + return self.value + + def to_save(self) -> int: + """ + Build a serializable object + + :Basic usage: + + >>> my_int = Int() + >>> my_int.to_save() + >>> my_int.set(34) + >>> my_int.to_save() + 34 + + :return: Current value + """ + return self.value + + def load(self, value: int) -> None: + """ + Load serialized value + + >>> my_int = Int() + >>> my_int.load(34) + >>> my_int.get() + 34 + + :param value: Value to load + :return: None + """ + if not self.check_value(value): + raise ValueError("Tentative de charger une donnée incompatible.") + self.value = value + + def __repr__(self): + if self.min is not None or self.max is not None: + return f'' + if self.values: + return f'' + return f'' diff --git a/config/config_types/list.py b/config/config_types/list.py new file mode 100644 index 0000000..0b6fa97 --- /dev/null +++ b/config/config_types/list.py @@ -0,0 +1,42 @@ +from typing import Type + +from config.config_types.base_type import BaseType + + +class List(BaseType): + type_: Type[BaseType] + + def __init__(self, type_, max_len=None): + self.type_ = type_ + self.max_len = max_len + self.values = None + + def check_value(self, value): + new_object = self.type_() + return new_object.check_value(value) + + def set(self, value): + """Check and set value""" + new_liste = [] + for v in value: + new_element = self.type_() + new_element.set(v) + new_liste.append(new_element) + self.values = new_liste + + def get(self): + """Get value""" + if self.values is None: + raise ValueError("Config non initialisée") + return [v.get() for v in self.values] + + def to_save(self): + """Build a serializable data to save""" + return [v.to_save() for v in self.values] + + def load(self, value): + """Fill with value""" + for v in value: + new_object = self.type_() + new_object.load(v) + self.values.append(new_object) diff --git a/config/config_types/str.py b/config/config_types/str.py new file mode 100644 index 0000000..5067482 --- /dev/null +++ b/config/config_types/str.py @@ -0,0 +1,35 @@ +from config.config_types.base_type import BaseType + + +class Str(BaseType): + def __init__(self): + self.value = None + + def check_value(self, value): + """Check if value is good""" + try: + str(value) + except ValueError: + return False + return True + + def set(self, value): + """Check and set value""" + if self.check_value(value): + self.value = value + return + raise ValueError("Tentative de définir une valeur incompatible") + + def get(self): + """Get value""" + return self.value + + def to_save(self): + """Build a serializable data to save""" + return self.value + + def load(self, value): + """Fill with value""" + if not self.check_value(value): + raise ValueError("Tentative de charger une donnée incompatible.") + self.value = value diff --git a/config/log_config.json b/config/log_config.json new file mode 100644 index 0000000..e0954b0 --- /dev/null +++ b/config/log_config.json @@ -0,0 +1,54 @@ +{ + "version": 1, + "disable_existing_loggers": false, + "formatters": { + "simple": { + "format": "%(asctime)s :: %(name)s :: %(levelname)s :: %(message)s" + } + }, + + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": "DEBUG", + "formatter": "simple", + "stream": "ext://sys.stdout" + }, + + "info_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "INFO", + "formatter": "simple", + "filename": "info.log", + "maxBytes": 1048576, + "backupCount": 20, + "encoding": "utf8" + }, + + "error_file_handler": { + "class": "logging.handlers.RotatingFileHandler", + "level": "ERROR", + "formatter": "simple", + "filename": "errors.log", + "maxBytes": 1048576, + "backupCount": 20, + "encoding": "utf8" + } + }, + + "loggers": { + "discord": { + "level":"ERROR", + "handlers":["console", "info_file_handler", "error_file_handler"] + }, + "LBI": { + "level":"DEBUG", + "handlers":["console", "info_file_handler", "error_file_handler"] + } + }, + + "root": { + "level": "INFO", + "handlers": [] + } +} \ No newline at end of file diff --git a/main.py b/main.py index d6edf3e..2a5b031 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,6 @@ #!/usr/bin/python3 +from __future__ import annotations + import asyncio import importlib import json @@ -12,7 +14,8 @@ import discord import humanize from packaging.version import Version -from config.FileSystem import FSConfig +from config import Config, config_types +from config.config_types import factory from errors import IncompatibleModule from modules.base import base_supported_type @@ -211,7 +214,7 @@ def event(func): setup_logging() -log_discord = logging.getLogger('discord') +log_discord = logging.getLogger('discord_types') log_LBI = logging.getLogger('LBI') log_communication = logging.getLogger('communication') @@ -222,6 +225,7 @@ def load_modules_info(): class LBI(discord.Client): + by_id: ClientById base_path = "data" debug = log_LBI.debug info = log_LBI.info @@ -229,21 +233,35 @@ class LBI(discord.Client): error = log_LBI.error critical = log_LBI.critical - def __init__(self, config=None, *args, **kwargs): + def __init__(self, config: Config = None, *args, **kwargs): super().__init__(*args, **kwargs) if config is None: - config = FSConfig(path="data/config.toml") + config = Config(path="data/config.toml") self.reloading = False - self.id = ClientById(self) + self.by_id = ClientById(self) self.ready = False # Content: {"module_name": {"module": imported module, "class": initialized class}} self.modules = {} + self.config = config - self.config.init( - {"modules": ["modules", "errors"], "prefix": "%", "admin_roles": [], "admin_users": [], "main_guild": 0, - "locale": "fr_FR.utf8"}) - locale.setlocale(locale.LC_TIME, self.config.locale) - humanize.i18n.activate(self.config.locale) + self.config.register("modules", factory(config_types.List, factory(config_types.Str))) + self.config.register("prefix", factory(config_types.Str)) + self.config.register("admin_roles", factory(config_types.List, factory(config_types.discord.Role, self))) + self.config.register("admin_users", factory(config_types.List, factory(config_types.discord.User, self))) + self.config.register("main_guild", factory(config_types.discord.Guild, self)) + self.config.register("locale", factory(config_types.Str)) + + self.config.set({ + "modules": ["modules", "errors"], + "prefix": "%", + "admin_roles": [], + "admin_users": [], + "main_guild": None, + "locale": "fr_FR.UTF8", + }) + + locale.setlocale(locale.LC_TIME, self.config['locale']) + humanize.i18n.activate(self.config['locale']) self.load_modules() @modules_edit @@ -367,10 +385,10 @@ class ClientById: :param id_: Id of message to find :type id_: int - :raises discord.NotFound: This exception is raised when a message is not found (or not accessible by bot) + :raises discord_types.NotFound: This exception is raised when a message is not found (or not accessible by bot) :rtype: discord.Message - :return: discord.Message instance if message is found. + :return: discord_types.Message instance if message is found. """ msg = None for channel in self.client.get_all_channels(): @@ -432,7 +450,6 @@ class ClientById: return None -client1 = LBI(max_messages=500000) class Communication(asyncio.Protocol): @@ -460,19 +477,20 @@ class Communication(asyncio.Protocol): def connection_lost(self, exc): print('%s: connection lost: %s' % (self.name, exc)) - -communication = Communication(client1) +if __name__ == "__main__": + client1 = LBI(max_messages=500000) + communication = Communication(client1) -async def start_bot(): - await client1.start(os.environ.get("DISCORD_TOKEN")) + async def start_bot(): + await client1.start(os.environ.get("DISCORD_TOKEN")) -print(os.path.join("/tmp", os.path.dirname(os.path.realpath(__file__))) + ".sock") + print(os.path.join("/tmp", os.path.dirname(os.path.realpath(__file__))) + ".sock") -loop = asyncio.get_event_loop() -t = loop.create_unix_server(Communication, - path=os.path.join("/tmp", os.path.dirname(os.path.realpath(__file__)) + ".sock")) -loop.run_until_complete(t) -loop.create_task(start_bot()) -loop.run_forever() + loop = asyncio.get_event_loop() + t = loop.create_unix_server(Communication, + path=os.path.join("/tmp", os.path.dirname(os.path.realpath(__file__)) + ".sock")) + loop.run_until_complete(t) + loop.create_task(start_bot()) + loop.run_forever() diff --git a/make.bat b/make.bat new file mode 100644 index 0000000..9534b01 --- /dev/null +++ b/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/modules/avalon/__init__.py b/modules/avalon/__init__.py index 9f7ef53..fe433a3 100644 --- a/modules/avalon/__init__.py +++ b/modules/avalon/__init__.py @@ -25,7 +25,7 @@ class MainClass(BaseClassPython): def __init__(self, client): super().__init__(client) - self.config.init({"spectate_channel": 0, + self.config.set({"spectate_channel": 0, "illustrations":{"merlin":"", "perceval":"", "gentil":"", @@ -43,7 +43,7 @@ class MainClass(BaseClassPython): "oberon":0, "mechant":0, "test":15}, - "test":{"merlin":"", + "test":{"merlin":"", "perceval":0, "gentil":0, "assassin":0, @@ -52,5 +52,5 @@ class MainClass(BaseClassPython): "oberon":0, "mechant":0, "test":15} - }) + }) diff --git a/modules/base/Base.py b/modules/base/Base.py index 406757c..db797c1 100644 --- a/modules/base/Base.py +++ b/modules/base/Base.py @@ -29,8 +29,8 @@ class BaseClass: :type client: LBI""" self.client = client self.objects = Objects(path=os.path.join("data", self.name.lower())) - self.config = Config(parent=self.client.config, name="mod-" + self.name.lower()) - self.config.init({"help_active": True, "color": 0x000000, "auth_everyone": False, "authorized_roles": [], + self.config = Config(path=os.path.join("data", self.name.lower(), "config.toml")) + self.config.set({"help_active": True, "color": 0x000000, "auth_everyone": False, "authorized_roles": [], "authorized_users": [], "command_text": self.name.lower(), "configured": False}) async def send_help(self, channel): diff --git a/modules/errors/__init__.py b/modules/errors/__init__.py index 3a6c8b2..0f31c21 100644 --- a/modules/errors/__init__.py +++ b/modules/errors/__init__.py @@ -13,7 +13,7 @@ class MainClass(BaseClassPython): authorized_users = [] authorized_roles = [] help = { - "description": "Montre toutes les erreurs du bot dans discord.", + "description": "Montre toutes les erreurs du bot dans discord_types.", "commands": { "`{prefix}{command}`": "Renvoie une erreur de test.", } @@ -21,7 +21,7 @@ class MainClass(BaseClassPython): def __init__(self, client): super().__init__(client) - self.config.init({"dev_chan": [], "memes": [""], "icon": ""}) + self.config.set({"dev_chan": [], "memes": [""], "icon": ""}) self.errorsList = None async def on_load(self): diff --git a/modules/newmember/__init__.py b/modules/newmember/__init__.py index 7a9f133..9ad8e18 100644 --- a/modules/newmember/__init__.py +++ b/modules/newmember/__init__.py @@ -11,7 +11,7 @@ class MainClass(BaseClassPython): def __init__(self, client): super().__init__(client) - self.config.init({"new_role": 0, + self.config.set({"new_role": 0, "motd": "Bienvenue !"}) async def on_ready(self): diff --git a/modules/perdu/__init__.py b/modules/perdu/__init__.py index 47d583c..5ab550f 100644 --- a/modules/perdu/__init__.py +++ b/modules/perdu/__init__.py @@ -5,7 +5,9 @@ import discord import humanize import matplotlib.pyplot as np +import config import utils.emojis +from config.config_types import factory from modules.base import BaseClassPython @@ -24,7 +26,8 @@ class MainClass(BaseClassPython): def __init__(self, client): super().__init__(client) - self.config.init({"channel": 0, "lost_role": 0, "min_delta": datetime.timedelta(minutes=26).total_seconds()}) + self.config.set({"channel": 0, "lost_role": 0, "min_delta": datetime.timedelta(minutes=26).total_seconds()}) + self.config.register("channel", factory(config.config_types.Channel, self.client)) self.history = {} async def on_message(self, message: discord.Message): diff --git a/modules/readrules/__init__.py b/modules/readrules/__init__.py index be2c401..3e17a0d 100644 --- a/modules/readrules/__init__.py +++ b/modules/readrules/__init__.py @@ -13,7 +13,7 @@ class MainClass(BaseClassPython): def __init__(self, client): super().__init__(client) - self.config.init({"accepted_role": 0, + self.config.set({"accepted_role": 0, "new_role": 0, "listen_chan": 0, "log_chan": 0, diff --git a/modules/roles/__init__.py b/modules/roles/__init__.py index 2211c36..067d070 100644 --- a/modules/roles/__init__.py +++ b/modules/roles/__init__.py @@ -34,7 +34,7 @@ class MainClass(BaseClassPython): def __init__(self, client): super().__init__(client) - self.config.init({"roles": {}}) + self.config.set({"roles": {}}) async def com_list(self, message, args, kwargs): response = discord.Embed(title="Roles disponibles", color=self.config.color) diff --git a/modules/rtfgd/__init__.py b/modules/rtfgd/__init__.py index db96c4e..7723561 100644 --- a/modules/rtfgd/__init__.py +++ b/modules/rtfgd/__init__.py @@ -16,7 +16,7 @@ class MainClass(BaseClassPython): def __init__(self, client): super().__init__(client) - self.config.init({"memes": []}) + self.config.set({"memes": []}) async def command(self, message, args, kwargs): await message.channel.send( diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..2bed0f3 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --doctest-modules \ No newline at end of file diff --git a/source/api/config.config_types.discord_types.rst b/source/api/config.config_types.discord_types.rst new file mode 100644 index 0000000..0753f7f --- /dev/null +++ b/source/api/config.config_types.discord_types.rst @@ -0,0 +1,10 @@ +config.config\_types.discord package +==================================== + +Module contents +--------------- + +.. automodule:: config.config_types.discord_types + :members: + :undoc-members: + :show-inheritance: diff --git a/source/api/config.config_types.rst b/source/api/config.config_types.rst new file mode 100644 index 0000000..2eb808a --- /dev/null +++ b/source/api/config.config_types.rst @@ -0,0 +1,20 @@ +config.config\_types package +============================ + +Module contents +--------------- + +.. automodule:: config.config_types + :members: + :undoc-members: + :show-inheritance: + + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + config.config_types.discord_types + diff --git a/source/api/config.rst b/source/api/config.rst new file mode 100644 index 0000000..19e34a8 --- /dev/null +++ b/source/api/config.rst @@ -0,0 +1,19 @@ +config package +============== + + +Module contents +--------------- + +.. automodule:: config + :members: + :undoc-members: + :show-inheritance: + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + config.config_types \ No newline at end of file diff --git a/source/api/modules.rst b/source/api/modules.rst new file mode 100644 index 0000000..ed29e2c --- /dev/null +++ b/source/api/modules.rst @@ -0,0 +1,8 @@ +API Reference +============= + +.. toctree:: + :maxdepth: 4 + + config + utils diff --git a/source/api/utils.emojis.rst b/source/api/utils.emojis.rst new file mode 100644 index 0000000..6cbb6a6 --- /dev/null +++ b/source/api/utils.emojis.rst @@ -0,0 +1,10 @@ +utils.emojis package +============================ + +Module contents +--------------- + +.. automodule:: utils.emojis + :members: + :undoc-members: + :show-inheritance: diff --git a/source/api/utils.rst b/source/api/utils.rst new file mode 100644 index 0000000..0beb23c --- /dev/null +++ b/source/api/utils.rst @@ -0,0 +1,19 @@ +utils package +============= + +Module contents +--------------- + +.. automodule:: utils + :members: + :undoc-members: + :show-inheritance: + + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + utils.emojis diff --git a/source/conf.py b/source/conf.py new file mode 100644 index 0000000..3775b97 --- /dev/null +++ b/source/conf.py @@ -0,0 +1,59 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +sys.path.insert(0, os.path.abspath('../')) + +# -- Project information ----------------------------------------------------- + +project = 'Python Discord Bot' +copyright = '2020, Chauvet Louis , Suwako' +author = 'Chauvet Louis , Suwako' + +# The full version, including alpha/beta/rc tags +release = '0.0.1' + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx_autodoc_typehints', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'classic' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +pygments_style = 'sphinx' +set_type_checking_flag = True +autoclass_content = 'both' \ No newline at end of file diff --git a/source/index.rst b/source/index.rst new file mode 100644 index 0000000..0f2421a --- /dev/null +++ b/source/index.rst @@ -0,0 +1,34 @@ +.. Python Discord Bot documentation master file, created by + sphinx-quickstart on Mon Apr 13 12:55:17 2020. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Python Discord Bot's documentation! +============================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + module_creation/index + api/modules + +Introduction +============ + +"Python Discord Bot" is a fully modular, self-hostable discord bot. + +Its goal is to provide a solid and minimal base (only error handling, help, modules and configuration management) and to provide a large amount of modules. + +In addition to being fully modular, this bot is meant to be a single server, in order to allow advanced configuration and simple management of private messages (many modules are games that need to use private messages, and it wouldn't be nice to add a choice of server for each action). + +For users, nothing could be simpler, you just install, register your bot on discordapp.com and let yourself be guided by !config. + +For developers, all the documentation is here, and the source code is fully documented. + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/source/module_creation/index.rst b/source/module_creation/index.rst new file mode 100644 index 0000000..dfb87e6 --- /dev/null +++ b/source/module_creation/index.rst @@ -0,0 +1,8 @@ +Module creation +=============== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + intro diff --git a/source/module_creation/intro.rst b/source/module_creation/intro.rst new file mode 100644 index 0000000..4cf956c --- /dev/null +++ b/source/module_creation/intro.rst @@ -0,0 +1,67 @@ +Introduction +============ + +Creating a module is relatively simple: just create a python package (a folder that contains a ``__init__.py`` file) in +the modules folder, insert a ``version.json`` file (which will allow you to add dependencies and general information for +your module) and have a MainClass class in the ``__init__.py`` file. + +So the next step is to create the :py:class:`MainClass`, which inherits from :py:class:`BaseClassPython`, here is a minimal example: + +.. code-block:: python + :linenos: + + class MainClass: + name = "MyFirstModule" + help = { + "description": "My first module", + "commands": { + } + } + +As you can see it's very simple, from now on you can start the bot and load the module. + +Currently it does nothing, so let's add a ``say`` command: + +.. code-block:: python + :linenos: + :emphasize-lines: 6,10,11 + + class MainClass: + name = "MyFirstModule" + help = { + "description": "My first module", + "commands": { + "{prefix}{command} say ": "Bot send message ", + } + } + + async def com_say(self, message, args, kwargs): + await message.channel.send(args[0]) + +You can now reload the module and test the command ``!myfirstmodule say "Hello world"``. + +You can see that without the quotation marks the returned message contains only the first word. Indeed each message is +processed to extract the module (here ``module``), the command (here ``say``) and the arguments. This is how the +arguments are processed: + + +``!mymodule say "Hello world" "Goodbye world"`` - ``args = ["Hello world", "Goodbye world"] kwargs=[]`` + +``!mymodule say --long-option -an -s "s value"`` - ``args = [] kwargs = [("long-option", None), ("a", None), ("n", None), ("s", "s value")]`` + +``!mymodule say -s "s value" "value"`` - ``args = ["value"] kwargs = [("s", "s value")]`` + +So let's add an ``-m`` option that adds the mention of the author to the message: + + +.. code-block:: python + :linenos: + :lineno-start: 10 + :emphasize-lines: 2,3,4 + + async def com_say(self, message, args, kwargs): + if 'm' in [k for k, v in kwargs]: + await message.channel.send(message.author.mention + args[0]) + return + await message.channel.send(args[0]) + diff --git a/utils/emojis.py b/utils/emojis.py index 166a020..37493a0 100644 --- a/utils/emojis.py +++ b/utils/emojis.py @@ -1,16 +1,38 @@ -NUMBERS = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟"] +from typing import Union -THUMBS_UP = "👍" -THUMBS_DOWN = "👎" -WHITE_CHECK_MARK = "✅" +NUMBERS = ["\u0030\u20e3", "\u0031\u20e3", "\u0032\u20e3", "\u0033\u20e3", "\u0034\u20e3", "\u0035\u20e3", + "\u0036\u20e3", "\u0037\u20e3", "\u0038\u20e3", "\u0039\u20e3", "\U0001f51f"] +MINUS = "\u2796" + +THUMBS_UP = "\U0001f44d" +THUMBS_DOWN = "\U0001f44e" +WHITE_CHECK_MARK = "\u2705" -def write_with_number(i): - raw = str(i) +def write_with_number(i: Union[int, float]): + """ + Write number with emoji + + :Basic usage: + + >>> write_with_number(23) + '2⃣3⃣' + >>> write_with_number(-23) + '➖2⃣3⃣' + >>> write_with_number(-23.34) + '➖2⃣3⃣.3⃣4⃣' + >>> write_with_number(-1234567890.098) + '➖1⃣2⃣3⃣4⃣5⃣6⃣7⃣8⃣9⃣0⃣.0⃣9⃣8⃣' + + :param i: number to write + :return: string with emojis + """ s = "" for c in str(i): - if raw == ".": + if c == ".": s += "." + elif c == "-": + s += MINUS else: s += NUMBERS[int(c)] return s