[V3 Config] Redesign "all_from_XXX" and "clear_all" methods (#1033)

* Added alternative to all_from_kind

* Returned dicts include default values

Also added docstrings

Also removed all_globals since it's kind of redundant and it wasn't working out for me

* Refactored clear_all

* Tests

* Tests again..

* Make all new methods coroutines
This commit is contained in:
Tobotimus 2017-10-20 14:22:58 +11:00 committed by Will
parent 13fef45e06
commit 815678584f
4 changed files with 257 additions and 112 deletions

View File

@ -63,7 +63,7 @@ class Bank:
If the bank is global, it will become per-guild If the bank is global, it will become per-guild
If the bank is per-guild, it will become global""" If the bank is per-guild, it will become global"""
cur_setting = await bank.is_global() cur_setting = await bank.is_global()
await bank.set_global(not cur_setting, ctx.author) await bank.set_global(not cur_setting)
word = _("per-guild") if cur_setting else _("global") word = _("per-guild") if cur_setting else _("global")

View File

@ -322,7 +322,7 @@ async def get_guild_accounts(guild: discord.Guild) -> List[Account]:
raise RuntimeError("The bank is currently global.") raise RuntimeError("The bank is currently global.")
ret = [] ret = []
accs = await _conf.member(guild.owner).all_from_kind() accs = await _conf.all_members(guild)
for user_id, acc in accs.items(): for user_id, acc in accs.items():
acc_data = acc.copy() # There ya go kowlin acc_data = acc.copy() # There ya go kowlin
acc_data['created_at'] = _decode_time(acc_data['created_at']) acc_data['created_at'] = _decode_time(acc_data['created_at'])
@ -353,7 +353,7 @@ async def get_global_accounts(user: discord.User) -> List[Account]:
raise RuntimeError("The bank is not currently global.") raise RuntimeError("The bank is not currently global.")
ret = [] ret = []
accs = await _conf.user(user).all_from_kind() # this is a dict of user -> acc accs = await _conf.all_users() # this is a dict of user -> acc
for user_id, acc in accs.items(): for user_id, acc in accs.items():
acc_data = acc.copy() acc_data = acc.copy()
acc_data['created_at'] = _decode_time(acc_data['created_at']) acc_data['created_at'] = _decode_time(acc_data['created_at'])
@ -412,8 +412,6 @@ async def is_global() -> bool:
async def set_global(global_: bool, user: Union[discord.User, discord.Member]) -> bool: async def set_global(global_: bool, user: Union[discord.User, discord.Member]) -> bool:
"""Set global status of the bank. """Set global status of the bank.
Requires the user parameter for technical reasons.
.. important:: .. important::
All accounts are reset when you switch! All accounts are reset when you switch!
@ -422,8 +420,6 @@ async def set_global(global_: bool, user: Union[discord.User, discord.Member]) -
---------- ----------
global_ : bool global_ : bool
:code:`True` will set bank to global mode. :code:`True` will set bank to global mode.
user : `discord.User` or `discord.Member`
Must be a Member object if changing TO global mode.
Returns Returns
------- -------
@ -440,12 +436,9 @@ async def set_global(global_: bool, user: Union[discord.User, discord.Member]) -
return global_ return global_
if is_global(): if is_global():
await _conf.user(user).clear_all() await _conf.clear_all_users()
elif isinstance(user, discord.Member):
await _conf.member(user).clear_all()
else: else:
raise RuntimeError("You must provide a member if you're changing to global" await _conf.clear_all_members()
" bank mode.")
await _conf.is_global.set(global_) await _conf.is_global.set(global_)
return global_ return global_

View File

@ -137,7 +137,7 @@ class Group(Value):
# noinspection PyTypeChecker # noinspection PyTypeChecker
def __getattr__(self, item: str) -> Union["Group", Value]: def __getattr__(self, item: str) -> Union["Group", Value]:
"""Get an attribute of this group. """Get an attribute of this group.
This special method is called whenever dot notation is used on this This special method is called whenever dot notation is used on this
object. object.
@ -145,13 +145,13 @@ class Group(Value):
---------- ----------
item : str item : str
The name of the attribute being accessed. The name of the attribute being accessed.
Returns Returns
------- -------
`Group` or `Value` `Group` or `Value`
A child value of this Group. This, of course, can be another A child value of this Group. This, of course, can be another
`Group`, due to Config's composite pattern. `Group`, due to Config's composite pattern.
Raises Raises
------ ------
AttributeError AttributeError
@ -260,7 +260,7 @@ class Group(Value):
If this is :code:`True` this function will return a coroutine that If this is :code:`True` this function will return a coroutine that
resolves to a "real" data value when awaited. If :code:`False`, resolves to a "real" data value when awaited. If :code:`False`,
this method acts the same as `__getattr__`. this method acts the same as `__getattr__`.
Returns Returns
------- -------
`types.coroutine` or `Value` or `Group` `types.coroutine` or `Value` or `Group`
@ -292,36 +292,6 @@ class Group(Value):
defaults.update(await self()) defaults.update(await self())
return defaults return defaults
async def all_from_kind(self) -> dict:
"""Get all data from this group and its siblings.
.. important::
This method is overridden in `MemberGroup.all_from_kind` and
functions slightly differently.
Note
----
The return value of this method will include registered defaults
for groups which have not had their values set.
Returns
-------
dict
A dict of :code:`ID -> data`, with the data being a dict
of the group's raw values.
"""
# noinspection PyTypeChecker
all_from_kind = await self._super_group()
for k, v in all_from_kind.items():
defaults = self.defaults
defaults.update(v)
all_from_kind[k] = defaults
return all_from_kind
async def set(self, value): async def set(self, value):
if not isinstance(value, dict): if not isinstance(value, dict):
raise ValueError( raise ValueError(
@ -331,14 +301,14 @@ class Group(Value):
async def set_attr(self, item: str, value): async def set_attr(self, item: str, value):
"""Set an attribute by its name. """Set an attribute by its name.
Similar to `get_attr` in the way it can be used to dynamically set Similar to `get_attr` in the way it can be used to dynamically set
attributes by name. attributes by name.
Note Note
---- ----
Use of this method should be avoided wherever possible. Use of this method should be avoided wherever possible.
Parameters Parameters
---------- ----------
item : str item : str
@ -358,17 +328,10 @@ class Group(Value):
""" """
await self.set({}) await self.set({})
async def clear_all(self):
"""Wipe all data from this group and its siblings.
If used on a global group, this method wipes all data from all groups.
"""
await self._super_group.set({})
class MemberGroup(Group): class MemberGroup(Group):
"""A specific group class for use with member data only. """A specific group class for use with member data only.
Inherits from `Group`. In this group data is stored as Inherits from `Group`. In this group data is stored as
:code:`GUILD_ID -> MEMBER_ID -> data`. :code:`GUILD_ID -> MEMBER_ID -> data`.
""" """
@ -392,41 +355,6 @@ class MemberGroup(Group):
) )
return group_obj return group_obj
async def all_guilds(self) -> dict:
"""Get a dict of :code:`GUILD_ID -> MEMBER_ID -> data`.
Note
----
The return value of this method will include registered defaults
for groups which have not had their values set.
Returns
-------
dict
A dict of data from all members from all guilds.
"""
# noinspection PyTypeChecker
return await super().all_from_kind()
async def all_from_kind(self) -> dict:
"""Get a dict of all members from the same guild as the given one.
Note
----
The return value of this method will include registered defaults
for groups which have not had their values set.
Returns
-------
dict
A dict of :code:`MEMBER_ID -> data`.
"""
guild_member = await super().all_from_kind()
return guild_member.get(self.identifiers[-2], {})
class Config: class Config:
"""Configuration manager for cogs and Red. """Configuration manager for cogs and Red.
@ -501,7 +429,7 @@ class Config:
force_registration : `bool`, optional force_registration : `bool`, optional
Should config require registration of data keys before allowing you Should config require registration of data keys before allowing you
to get/set values? See `force_registration`. to get/set values? See `force_registration`.
Returns Returns
------- -------
Config Config
@ -519,11 +447,13 @@ 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 """Get a Config instance for a core module.
All core modules that require a config instance should use this
classmethod instead of `get_conf`. classmethod instead of `get_conf`.
identifier : int Parameters
See `get_conf`. ----------
force_registration : `bool`, optional force_registration : `bool`, optional
See `force_registration`. See `force_registration`.
@ -535,17 +465,17 @@ class Config:
def __getattr__(self, item: str) -> Union[Group, Value]: def __getattr__(self, item: str) -> Union[Group, Value]:
"""Same as `group.__getattr__` except for global data. """Same as `group.__getattr__` except for global data.
Parameters Parameters
---------- ----------
item : str item : str
The attribute you want to get. The attribute you want to get.
Returns Returns
------- -------
`Group` or `Value` `Group` or `Value`
The value for the attribute you want to retrieve The value for the attribute you want to retrieve
Raises Raises
------ ------
AttributeError AttributeError
@ -659,7 +589,7 @@ class Config:
def register_guild(self, **kwargs): def register_guild(self, **kwargs):
"""Register default values on a per-guild level. """Register default values on a per-guild level.
See :py:meth:`register_global` for more details. See :py:meth:`register_global` for more details.
""" """
self._register_default(self.GUILD, **kwargs) self._register_default(self.GUILD, **kwargs)
@ -681,16 +611,16 @@ class Config:
def register_user(self, **kwargs): def register_user(self, **kwargs):
"""Registers default values on a per-user level. """Registers default values on a per-user level.
This means that each user's data is guild-independent. This means that each user's data is guild-independent.
See `register_global` for more details. See `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-member level. """Registers default values on a per-member level.
This means that each user's data is guild-dependent. This means that each user's data is guild-dependent.
See `register_global` for more details. See `register_global` for more details.
@ -720,7 +650,7 @@ class Config:
def channel(self, channel: discord.TextChannel) -> Group: def channel(self, channel: discord.TextChannel) -> Group:
"""Returns a `Group` for the given channel. """Returns a `Group` for the given channel.
This does not discriminate between text and voice channels. This does not discriminate between text and voice channels.
Parameters Parameters
@ -765,3 +695,226 @@ class Config:
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)
async def _all_from_scope(self, scope: str):
"""Get a dict of all values from a particular scope of data.
:code:`scope` must be one of the constants attributed to
this class, i.e. :code:`GUILD`, :code:`MEMBER` et cetera.
IDs as keys in the returned dict are casted to `int` for convenience.
Default values are also mixed into the data if they have not yet been
overwritten.
"""
group = self._get_base_group(scope)
dict_ = await group()
ret = {}
for k, v in dict_.items():
data = group.defaults
data.update(v)
ret[int(k)] = data
return ret
async def all_guilds(self) -> dict:
"""Get all guild data as a dict.
Note
----
The return value of this method will include registered defaults for
values which have not yet been set.
Returns
-------
dict
A dictionary in the form {`int`: `dict`} mapping
:code:`GUILD_ID -> data`.
"""
return await self._all_from_scope(self.GUILD)
async def all_channels(self) -> dict:
"""Get all channel data as a dict.
Note
----
The return value of this method will include registered defaults for
values which have not yet been set.
Returns
-------
dict
A dictionary in the form {`int`: `dict`} mapping
:code:`CHANNEL_ID -> data`.
"""
return await self._all_from_scope(self.CHANNEL)
async def all_roles(self) -> dict:
"""Get all role data as a dict.
Note
----
The return value of this method will include registered defaults for
values which have not yet been set.
Returns
-------
dict
A dictionary in the form {`int`: `dict`} mapping
:code:`ROLE_ID -> data`.
"""
return await self._all_from_scope(self.ROLE)
async def all_users(self) -> dict:
"""Get all user data as a dict.
Note
----
The return value of this method will include registered defaults for
values which have not yet been set.
Returns
-------
dict
A dictionary in the form {`int`: `dict`} mapping
:code:`USER_ID -> data`.
"""
return await self._all_from_scope(self.USER)
def _all_members_from_guild(self, group: Group, guild_data: dict) -> dict:
ret = {}
for member_id, member_data in guild_data.items():
new_member_data = group.defaults
new_member_data.update(member_data)
ret[int(member_id)] = new_member_data
return ret
async def all_members(self, guild: discord.Guild=None) -> dict:
"""Get data for all members.
If :code:`guild` is specified, only the data for the members of that
guild will be returned. As such, the dict will map
:code:`MEMBER_ID -> data`. Otherwise, the dict maps
:code:`GUILD_ID -> MEMBER_ID -> data`.
Note
----
The return value of this method will include registered defaults for
values which have not yet been set.
Parameters
----------
guild : `discord.Guild`, optional
The guild to get the member data from. Can be omitted if data
from every member of all guilds is desired.
Returns
-------
dict
A dictionary of all specified member data.
"""
ret = {}
if guild is None:
group = self._get_base_group(self.MEMBER)
dict_ = await group()
for guild_id, guild_data in dict_.items():
ret[int(guild_id)] = self._all_members_from_guild(
group, guild_data)
else:
group = self._get_base_group(self.MEMBER, guild.id)
guild_data = await group()
ret = self._all_members_from_guild(group, guild_data)
return ret
async def _clear_scope(self, *scopes: str):
"""Clear all data in a particular scope.
The only situation where a second scope should be passed in is if
member data from a specific guild is being cleared.
If no scopes are passed, then all data is cleared from every scope.
Parameters
----------
*scopes : str, optional
The scope of the data. Generally only one scope needs to be
provided, a second only necessary for clearing member data
of a specific guild.
**Leaving blank removes all data from this Config instance.**
"""
if not scopes:
group = Group(identifiers=(self.unique_identifier),
defaults={},
spawner=self.spawner)
else:
group = self._get_base_group(*scopes)
await group.set({})
async def clear_all(self):
"""Clear all data from this Config instance.
This resets all data to its registered defaults.
.. important::
This cannot be undone.
"""
await self._clear_scope()
async def clear_all_globals(self):
"""Clear all global data.
This resets all global data to its registered defaults.
"""
await self._clear_scope(self.GLOBAL)
async def clear_all_guilds(self):
"""Clear all guild data.
This resets all guild data to its registered defaults.
"""
await self._clear_scope(self.GUILD)
async def clear_all_channels(self):
"""Clear all channel data.
This resets all channel data to its registered defaults.
"""
await self._clear_scope(self.CHANNEL)
async def clear_all_roles(self):
"""Clear all role data.
This resets all role data to its registered defaults.
"""
await self._clear_scope(self.ROLE)
async def clear_all_users(self):
"""Clear all user data.
This resets all user data to its registered defaults.
"""
await self._clear_scope(self.USER)
async def clear_all_members(self, guild: discord.Guild=None):
"""Clear all member data.
This resets all specified member data to its registered defaults.
Parameters
----------
guild : `discord.Guild`, optional
The guild to clear member data from. Omit to clear member data from
all guilds.
"""
if guild is not None:
await self._clear_scope(self.MEMBER, guild.id)
return
await self._clear_scope(self.MEMBER)

View File

@ -234,16 +234,16 @@ async def test_get_dynamic_attr(config):
async def test_membergroup_allguilds(config, empty_member): async def test_membergroup_allguilds(config, empty_member):
await config.member(empty_member).foo.set(False) await config.member(empty_member).foo.set(False)
all_servers = await config.member(empty_member).all_guilds() all_servers = await config.all_members()
assert str(empty_member.guild.id) in all_servers assert empty_member.guild.id in all_servers
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_membergroup_allmembers(config, empty_member): async def test_membergroup_allmembers(config, empty_member):
await config.member(empty_member).foo.set(False) await config.member(empty_member).foo.set(False)
all_members = await config.member(empty_member).all_from_kind() all_members = await config.all_members(empty_member.guild)
assert str(empty_member.id) in all_members assert empty_member.id in all_members
# Clearing testing # Clearing testing
@ -291,11 +291,11 @@ async def test_member_clear_all(config, member_factory):
server_ids.append(member.guild.id) server_ids.append(member.guild.id)
member = member_factory.get() member = member_factory.get()
assert len(await config.member(member).all_guilds()) == len(server_ids) assert len(await config.all_members()) == len(server_ids)
await config.member(member).clear_all() await config.clear_all_members()
assert len(await config.member(member).all_guilds()) == 0 assert len(await config.all_members()) == 0
# Get All testing # Get All testing
@ -309,8 +309,7 @@ async def test_user_get_all_from_kind(config, user_factory):
user = user_factory.get() user = user_factory.get()
await config.user(user).foo.set(True) await config.user(user).foo.set(True)
user = user_factory.get() all_data = await config.all_users()
all_data = await config.user(user).all_from_kind()
assert len(all_data) == 5 assert len(all_data) == 5