[Docs] Copy over config docs from red-api-docs (#899)

* Update config.Config docs

* Set group docstrings

* Update Value docs
This commit is contained in:
Will 2017-08-11 01:39:43 -04:00 committed by GitHub
parent 7e05903c61
commit cf8e11238c
2 changed files with 293 additions and 49 deletions

View File

@ -13,6 +13,21 @@ log = logging.getLogger("red.config")
class Value: 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): def __init__(self, identifiers: Tuple[str], default_value, spawner):
self._identifiers = identifiers self._identifiers = identifiers
self.default = default_value self.default = default_value
@ -24,6 +39,26 @@ class Value:
return tuple(str(i) for i in self._identifiers) return tuple(str(i) for i in self._identifiers)
def __call__(self, default=None): 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() driver = self.spawner.get_driver()
try: try:
ret = driver.get(self.identifiers) ret = driver.get(self.identifiers)
@ -32,11 +67,34 @@ class Value:
return ret return ret
async def set(self, value): 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() driver = self.spawner.get_driver()
await driver.set(self.identifiers, value) await driver.set(self.identifiers, value)
class Group(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], def __init__(self, identifiers: Tuple[str],
defaults: dict, defaults: dict,
spawner, spawner,
@ -50,13 +108,17 @@ class Group(Value):
# noinspection PyTypeChecker # noinspection PyTypeChecker
def __getattr__(self, item: str) -> Union["Group", Value]: def __getattr__(self, item: str) -> Union["Group", Value]:
""" """
Takes in the next accessible item. If it's found to be a Group Takes in the next accessible item.
we return another Group object. If it's found to be a Value
we return a Value object. If it is not found and 1. If it's found to be a group of data we return another :py:class:`Group` object.
force_registration is True then we raise AttributeException, 2. If it's found to be a data value we return a :py:class:`.Value` object.
otherwise return a Value object. 3. If it is not found and :py:attr:`force_registration` is :python:`True` then we raise
:param item: :py:exc:`AttributeError`.
:return: 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_group = self.is_group(item)
is_value = not is_group and self.is_value(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: def is_group(self, item: str) -> bool:
""" """
Determines if an attribute access is pointing at a registered group. A helper method for :py:meth:`__getattr__`. Most developers will have no need to use this.
:param item:
:return: :param str item:
See :py:meth:`__getattr__`.
""" """
default = self.defaults.get(item) default = self.defaults.get(item)
return isinstance(default, dict) return isinstance(default, dict)
def is_value(self, item: str) -> bool: def is_value(self, item: str) -> bool:
""" """
Determines if an attribute access is pointing at a registered value. A helper method for :py:meth:`__getattr__`. Most developers will have no need to use this.
:param item:
:return: :param str item:
See :py:meth:`__getattr__`.
""" """
try: try:
default = self.defaults[item] default = self.defaults[item]
@ -120,12 +184,30 @@ class Group(Value):
def get_attr(self, item: str, default=None, resolve=True): def get_attr(self, item: str, default=None, resolve=True):
""" """
You should avoid this function whenever possible. This is available to use as an alternative to using normal Python attribute access. It is required if you find
:param item: 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: :param default:
This is an optional override to the registered default for this item.
:param resolve: :param resolve:
If this is True, actual data will be returned, if false a Group/Value will be returned. If this is :code:`True` this function will return a "real" data value, if :code:`False` this
:return: 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) value = getattr(self, item)
if resolve: if resolve:
@ -135,17 +217,19 @@ class Group(Value):
def all(self) -> dict: def all(self) -> dict:
""" """
Gets all data from current User/Member/Guild etc. This method allows you to get "all" of a particular group of data. It will return the dictionary of all data
:return: for a particular Guild/Channel/Role/User/Member etc.
:rtype: dict
""" """
return self() return self()
def all_from_kind(self) -> dict: def all_from_kind(self) -> dict:
""" """
Gets all entries of the given kind. If this kind is member This method allows you to get all data from all entries in a given Kind. It will return a dictionary of Kind
then this method returns all members from the same ID's -> data.
server.
:return: :rtype: dict
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
return self._super_group() return self._super_group()
@ -159,31 +243,35 @@ class Group(Value):
async def set_attr(self, item: str, value): async def set_attr(self, item: str, value):
""" """
You should avoid this function whenever possible. Please see :py:meth:`get_attr` for more information.
:param item:
:param value: .. note::
:return:
Use of this method should be avoided wherever possible.
""" """
value_obj = getattr(self, item) value_obj = getattr(self, item)
await value_obj.set(value) await value_obj.set(value)
async def clear(self): async def clear(self):
""" """
Wipes out data for the given entry in this category Wipes all data from the given Guild/Channel/Role/Member/User. If used on a global group, it will wipe all global
e.g. Guild/Role/User data.
:return:
""" """
await self.set({}) await self.set({})
async def clear_all(self): async def clear_all(self):
""" """
Removes all data from all entries. Wipes all data from all Guilds/Channels/Roles/Members/Users. If used on a global group, this method wipes all
:return: data from everything.
""" """
await self._super_group.set({}) await self._super_group.set({})
class MemberGroup(Group): 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 @property
def _super_group(self) -> Group: def _super_group(self) -> Group:
new_identifiers = self.identifiers[:2] new_identifiers = self.identifiers[:2]
@ -206,23 +294,51 @@ class MemberGroup(Group):
def all_guilds(self) -> dict: 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. :rtype: dict
:return:
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
return self._super_group() return self._super_group()
def all(self) -> dict: def all(self) -> dict:
"""
Returns the dict of all members in the same guild.
:return:
"""
# noinspection PyTypeChecker # noinspection PyTypeChecker
return self._guild_group() return self._guild_group()
class Config: 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" GLOBAL = "GLOBAL"
GUILD = "GUILD" GUILD = "GUILD"
CHANNEL = "TEXTCHANNEL" CHANNEL = "TEXTCHANNEL"
@ -246,13 +362,17 @@ class Config:
force_registration=False): force_registration=False):
""" """
Returns a Config instance based on a simplified set of initial Returns a Config instance based on a simplified set of initial
variables. variables.
:param cog_instance: :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. 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? of data keys before allowing you to get/set values?
:return: :return:
A new config object.
""" """
cog_name = cog_instance.__class__.__name__ cog_name = cog_instance.__class__.__name__
uuid = str(hash(identifier)) uuid = str(hash(identifier))
@ -264,6 +384,16 @@ class Config:
@classmethod @classmethod
def get_core_conf(cls, force_registration: bool=False): 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' core_data_path = Path.cwd() / 'core' / '.data'
driver_spawn = JSONDriver("Core", data_path_override=core_data_path) driver_spawn = JSONDriver("Core", data_path_override=core_data_path)
return cls(cog_name="Core", driver_spawn=driver_spawn, return cls(cog_name="Core", driver_spawn=driver_spawn,
@ -340,22 +470,74 @@ class Config:
self._update_defaults(to_add, self.defaults[key]) self._update_defaults(to_add, self.defaults[key])
def register_global(self, **kwargs): 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) self._register_default(self.GLOBAL, **kwargs)
def register_guild(self, **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) self._register_default(self.GUILD, **kwargs)
def register_channel(self, **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 # We may need to add a voice channel category later
self._register_default(self.CHANNEL, **kwargs) self._register_default(self.CHANNEL, **kwargs)
def register_role(self, **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) self._register_default(self.ROLE, **kwargs)
def register_user(self, **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) self._register_default(self.USER, **kwargs)
def register_member(self, **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) self._register_default(self.MEMBER, **kwargs)
def _get_base_group(self, key: str, *identifiers: str, def _get_base_group(self, key: str, *identifiers: str,
@ -369,18 +551,44 @@ class Config:
) )
def guild(self, guild: discord.Guild) -> Group: 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) return self._get_base_group(self.GUILD, guild.id)
def channel(self, channel: discord.TextChannel) -> Group: 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) return self._get_base_group(self.CHANNEL, channel.id)
def role(self, role: discord.Role) -> Group: 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) return self._get_base_group(self.ROLE, role.id)
def user(self, user: discord.User) -> Group: 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) return self._get_base_group(self.USER, user.id)
def member(self, member: discord.Member) -> MemberGroup: 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, return self._get_base_group(self.MEMBER, member.guild.id, member.id,
group_class=MemberGroup) group_class=MemberGroup)

View File

@ -1,17 +1,45 @@
.. config shite .. config shite
.. role:: python(code)
:language: python
======
Config 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 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 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 .. automodule:: core.config
@ -20,13 +48,21 @@ API Reference
.. autoclass:: Group .. autoclass:: Group
:members: :members:
:special-members:
.. autoclass:: MemberGroup
:members:
.. autoclass:: Value .. autoclass:: Value
:members: :members:
:special-members: __call__
================ ****************
Driver Reference Driver Reference
================ ****************
.. automodule:: core.drivers .. automodule:: core.drivers
.. autoclass:: red_base.BaseDriver
:members: