diff --git a/core/config.py b/core/config.py index 159d88b13..3fe245604 100644 --- a/core/config.py +++ b/core/config.py @@ -13,6 +13,21 @@ log = logging.getLogger("red.config") class Value: + """ + A singular "value" of data. + + .. py:attribute:: identifiers + + This attribute provides all the keys necessary to get a specific data element from a json document. + + .. py:attribute:: default + + The default value for the data element that :py:attr:`identifiers` points at. + + .. py:attribute:: spawner + + A reference to :py:attr:`.Config.spawner`. + """ def __init__(self, identifiers: Tuple[str], default_value, spawner): self._identifiers = identifiers self.default = default_value @@ -24,6 +39,26 @@ class Value: return tuple(str(i) for i in self._identifiers) def __call__(self, default=None): + """ + Each :py:class:`Value` object is created by the :py:meth:`Group.__getattr__` method. + The "real" data of the :py:class:`Value` object is accessed by this method. It is a replacement for a + :python:`get()` method. + + For example:: + + foo = conf.guild(some_guild).foo() + + # Is equivalent to this + + group_obj = conf.guild(some_guild) + value_obj = conf.foo + foo = value_obj() + + :param default: + This argument acts as an override for the registered default provided by :py:attr:`default`. This argument + is ignored if its value is :python:`None`. + :type default: Optional[object] + """ driver = self.spawner.get_driver() try: ret = driver.get(self.identifiers) @@ -32,11 +67,34 @@ class Value: return ret async def set(self, value): + """ + Sets the value of the data element indicate by :py:attr:`identifiers`. + + For example:: + + # Sets global value "foo" to False + await conf.foo.set(False) + + # Sets guild specific value of "bar" to True + await conf.guild(some_guild).bar.set(True) + """ driver = self.spawner.get_driver() await driver.set(self.identifiers, value) class Group(Value): + """ + A "group" of data, inherits from :py:class:`.Value` which means that all of the attributes and methods available + in :py:class:`.Value` are also available when working with a :py:class:`.Group` object. + + .. py:attribute:: defaults + + A dictionary of registered default values for this :py:class:`Group`. + + .. py:attribute:: force_registration + + See :py:attr:`.Config.force_registration`. + """ def __init__(self, identifiers: Tuple[str], defaults: dict, spawner, @@ -50,13 +108,17 @@ class Group(Value): # noinspection PyTypeChecker def __getattr__(self, item: str) -> Union["Group", Value]: """ - Takes in the next accessible item. If it's found to be a Group - we return another Group object. If it's found to be a Value - we return a Value object. If it is not found and - force_registration is True then we raise AttributeException, - otherwise return a Value object. - :param item: - :return: + Takes in the next accessible item. + + 1. If it's found to be a group of data we return another :py:class:`Group` object. + 2. If it's found to be a data value we return a :py:class:`.Value` object. + 3. If it is not found and :py:attr:`force_registration` is :python:`True` then we raise + :py:exc:`AttributeError`. + 4. Otherwise return a :py:class:`.Value` object. + + :param str item: + The name of the item a cog is attempting to access through normal Python attribute + access. """ is_group = self.is_group(item) is_value = not is_group and self.is_value(item) @@ -98,18 +160,20 @@ class Group(Value): def is_group(self, item: str) -> bool: """ - Determines if an attribute access is pointing at a registered group. - :param item: - :return: + A helper method for :py:meth:`__getattr__`. Most developers will have no need to use this. + + :param str item: + See :py:meth:`__getattr__`. """ default = self.defaults.get(item) return isinstance(default, dict) def is_value(self, item: str) -> bool: """ - Determines if an attribute access is pointing at a registered value. - :param item: - :return: + A helper method for :py:meth:`__getattr__`. Most developers will have no need to use this. + + :param str item: + See :py:meth:`__getattr__`. """ try: default = self.defaults[item] @@ -120,12 +184,30 @@ class Group(Value): def get_attr(self, item: str, default=None, resolve=True): """ - You should avoid this function whenever possible. - :param item: + This is available to use as an alternative to using normal Python attribute access. It is required if you find + a need for dynamic attribute access. + + .. note:: + + Use of this method should be avoided wherever possible. + + A possible use case:: + + @commands.command() + async def some_command(self, ctx, item: str): + user = ctx.author + + # Where the value of item is the name of the data field in Config + await ctx.send(self.conf.user(user).get_attr(item)) + + :param str item: + The name of the data field in :py:class:`.Config`. :param default: + This is an optional override to the registered default for this item. :param resolve: - If this is True, actual data will be returned, if false a Group/Value will be returned. - :return: + If this is :code:`True` this function will return a "real" data value, if :code:`False` this + function will return an instance of :py:class:`Group` or :py:class:`Value` depending on the + type of the "real" data value. """ value = getattr(self, item) if resolve: @@ -135,17 +217,19 @@ class Group(Value): def all(self) -> dict: """ - Gets all data from current User/Member/Guild etc. - :return: + This method allows you to get "all" of a particular group of data. It will return the dictionary of all data + for a particular Guild/Channel/Role/User/Member etc. + + :rtype: dict """ return self() def all_from_kind(self) -> dict: """ - Gets all entries of the given kind. If this kind is member - then this method returns all members from the same - server. - :return: + This method allows you to get all data from all entries in a given Kind. It will return a dictionary of Kind + ID's -> data. + + :rtype: dict """ # noinspection PyTypeChecker return self._super_group() @@ -159,31 +243,35 @@ class Group(Value): async def set_attr(self, item: str, value): """ - You should avoid this function whenever possible. - :param item: - :param value: - :return: + Please see :py:meth:`get_attr` for more information. + + .. note:: + + Use of this method should be avoided wherever possible. """ value_obj = getattr(self, item) await value_obj.set(value) async def clear(self): """ - Wipes out data for the given entry in this category - e.g. Guild/Role/User - :return: + Wipes all data from the given Guild/Channel/Role/Member/User. If used on a global group, it will wipe all global + data. """ await self.set({}) async def clear_all(self): """ - Removes all data from all entries. - :return: + Wipes all data from all Guilds/Channels/Roles/Members/Users. If used on a global group, this method wipes all + data from everything. """ await self._super_group.set({}) class MemberGroup(Group): + """ + A specific group class for use with member data only. Inherits from :py:class:`.Group`. In this group data is + stored as :code:`GUILD_ID -> MEMBER_ID -> data`. + """ @property def _super_group(self) -> Group: new_identifiers = self.identifiers[:2] @@ -206,23 +294,51 @@ class MemberGroup(Group): def all_guilds(self) -> dict: """ - Gets a dict of all guilds and members. + Returns a dict of :code:`GUILD_ID -> MEMBER_ID -> data`. - REMEMBER: ID's are stored in these dicts as STRINGS. - :return: + :rtype: dict """ # noinspection PyTypeChecker return self._super_group() def all(self) -> dict: - """ - Returns the dict of all members in the same guild. - :return: - """ # noinspection PyTypeChecker return self._guild_group() + class Config: + """ + You should always use :func:`get_conf` or :func:`get_core_conf` to initialize a Config object. + + .. important:: + Most config data should be accessed through its respective group method (e.g. :py:meth:`guild`) + however the process for accessing global data is a bit different. There is no :python:`global` method + because global data is accessed by normal attribute access:: + + conf.foo() + + .. py:attribute:: cog_name + + The name of the cog that has requested a :py:class:`.Config` object. + + .. py:attribute:: unique_identifier + + Unique identifier provided to differentiate cog data when name conflicts occur. + + .. py:attribute:: spawner + + A callable object that returns some driver that implements :py:class:`.drivers.red_base.BaseDriver`. + + .. py:attribute:: force_registration + + A boolean that determines if :py:class:`.Config` should throw an error if a cog attempts to access an attribute + which has not been previously registered. + + .. note:: + + **You should use this.** By enabling force registration you give :py:class:`.Config` the ability to alert + you instantly if you've made a typo when attempting to access data. + """ GLOBAL = "GLOBAL" GUILD = "GUILD" CHANNEL = "TEXTCHANNEL" @@ -246,13 +362,17 @@ class Config: force_registration=False): """ Returns a Config instance based on a simplified set of initial - variables. + variables. + :param cog_instance: - :param identifier: Any random integer, used to keep your data + :param identifier: + Any random integer, used to keep your data distinct from any other cog with the same name. - :param force_registration: Should config require registration + :param force_registration: + Should config require registration of data keys before allowing you to get/set values? :return: + A new config object. """ cog_name = cog_instance.__class__.__name__ uuid = str(hash(identifier)) @@ -264,6 +384,16 @@ class Config: @classmethod def get_core_conf(cls, force_registration: bool=False): + """ + All core modules that require a config instance should use this classmethod instead of + :py:meth:`get_conf` + + :param int identifier: + See :py:meth:`get_conf` + :param force_registration: + See :py:attr:`force_registration` + :type force_registration: Optional[bool] + """ core_data_path = Path.cwd() / 'core' / '.data' driver_spawn = JSONDriver("Core", data_path_override=core_data_path) return cls(cog_name="Core", driver_spawn=driver_spawn, @@ -340,22 +470,74 @@ class Config: self._update_defaults(to_add, self.defaults[key]) def register_global(self, **kwargs): + """ + Registers default values for attributes you wish to store in :py:class:`.Config` at a global level. + + You can register a single value or multiple values:: + + conf.register_global( + foo=True + ) + + conf.register_global( + bar=False, + baz=None + ) + + You can also now register nested values:: + + defaults = { + "foo": { + "bar": True, + "baz": False + } + } + + # Will register `foo.bar` == True and `foo.baz` == False + conf.register_global( + **defaults + ) + + You can do the same thing without a :python:`defaults` dict by using double underscore as a variable + name separator:: + + # This is equivalent to the previous example + conf.register_global( + foo__bar=True, + foo__baz=False + ) + """ self._register_default(self.GLOBAL, **kwargs) def register_guild(self, **kwargs): + """ + Registers default values on a per-guild level. See :py:meth:`register_global` for more details. + """ self._register_default(self.GUILD, **kwargs) def register_channel(self, **kwargs): + """ + Registers default values on a per-guild level. See :py:meth:`register_global` for more details. + """ # We may need to add a voice channel category later self._register_default(self.CHANNEL, **kwargs) def register_role(self, **kwargs): + """ + Registers default values on a per-guild level. See :py:meth:`register_global` for more details. + """ self._register_default(self.ROLE, **kwargs) def register_user(self, **kwargs): + """ + Registers default values on a per-guild level. See :py:meth:`register_global` for more details. + """ self._register_default(self.USER, **kwargs) def register_member(self, **kwargs): + """ + Registers default values on a per-guild level. See :py:meth:`register_global` for more details. + """ self._register_default(self.MEMBER, **kwargs) def _get_base_group(self, key: str, *identifiers: str, @@ -369,18 +551,44 @@ class Config: ) def guild(self, guild: discord.Guild) -> Group: + """ + Returns a :py:class:`.Group` for the given guild. + + :param discord.Guild guild: A discord.py guild object. + """ return self._get_base_group(self.GUILD, guild.id) def channel(self, channel: discord.TextChannel) -> Group: + """ + Returns a :py:class:`.Group` for the given channel. This does not currently support differences between + text and voice channels. + + :param discord.TextChannel channel: A discord.py text channel object. + """ return self._get_base_group(self.CHANNEL, channel.id) def role(self, role: discord.Role) -> Group: + """ + Returns a :py:class:`.Group` for the given role. + + :param discord.Role role: A discord.py role object. + """ return self._get_base_group(self.ROLE, role.id) def user(self, user: discord.User) -> Group: + """ + Returns a :py:class:`.Group` for the given user. + + :param discord.User user: A discord.py user object. + """ return self._get_base_group(self.USER, user.id) def member(self, member: discord.Member) -> MemberGroup: + """ + Returns a :py:class:`.MemberGroup` for the given member. + + :param discord.Member member: A discord.py member object. + """ return self._get_base_group(self.MEMBER, member.guild.id, member.id, group_class=MemberGroup) diff --git a/docs/framework_config.rst b/docs/framework_config.rst index d4e999e96..bee691096 100644 --- a/docs/framework_config.rst +++ b/docs/framework_config.rst @@ -1,17 +1,45 @@ .. config shite +.. role:: python(code) + :language: python + +====== Config ====== -=========== +Config was introduced in V3 as a way to make data storage easier and safer for all developers regardless of skill level. +It will take some getting used to as the syntax is entirely different from what Red has used before, but we believe +Config will be extremely beneficial to both cog developers and end users in the long run. + +*********** Basic Usage -=========== +*********** -Stuff goes here +.. code-block:: python -============= + from core import Config + + class MyCog: + def __init__(self): + self.config = Config.get_conf(self, identifier=1234567890) + + self.config.register_global( + foo=True + ) + + @commands.command() + async def return_some_data(self, ctx): + await ctx.send(config.foo()) + +************* API Reference -============= +************* + +.. important:: + + Before we begin with the nitty gritty API Reference, you should know that there are tons of working code examples + inside the bot itself! Simply take a peek inside of the :code:`tests/core/test_config.py` file for examples of using + Config in all kinds of ways. .. automodule:: core.config @@ -20,13 +48,21 @@ API Reference .. autoclass:: Group :members: + :special-members: + +.. autoclass:: MemberGroup + :members: .. autoclass:: Value :members: + :special-members: __call__ -================ +**************** Driver Reference -================ +**************** .. automodule:: core.drivers + +.. autoclass:: red_base.BaseDriver + :members: