[V3] NumPy Docstrings (#1032)

* ALL THE DOCSTRINGS

* Remove imports in drivers package

* Fixed build warnings
This commit is contained in:
Tobotimus 2017-10-18 13:01:59 +11:00 committed by Will
parent 684004d614
commit c80684a129
14 changed files with 976 additions and 560 deletions

View File

@ -37,6 +37,7 @@ extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
'sphinx.ext.intersphinx', 'sphinx.ext.intersphinx',
'sphinx.ext.viewcode', 'sphinx.ext.viewcode',
'sphinx.ext.napoleon',
'sphinxcontrib.asyncio' 'sphinxcontrib.asyncio'
] ]
@ -84,6 +85,9 @@ pygments_style = 'sphinx'
# If true, `todo` and `todoList` produce output, else they produce nothing. # If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False todo_include_todos = False
# Role which is assigned when you make a simple reference within backticks
default_role = "any"
# -- Options for HTML output ---------------------------------------------- # -- Options for HTML output ----------------------------------------------

View File

@ -192,5 +192,4 @@ Driver Reference
.. automodule:: redbot.core.drivers .. automodule:: redbot.core.drivers
.. autoclass:: red_base.BaseDriver .. autoclass:: redbot.core.drivers.red_base.BaseDriver
:members:

View File

@ -27,11 +27,15 @@ Installing Red
1. Open a command prompt (open Start, search for "command prompt", then click it) 1. Open a command prompt (open Start, search for "command prompt", then click it)
2. Run the appropriate command, depending on if you want audio or not 2. Run the appropriate command, depending on if you want audio or not
* No audio: :code:`python -m pip install Red-DiscordBot` * No audio: :code:`python -m pip install Red-DiscordBot`
* Audio: :code:`python -m pip install Red-DiscordBot[voice]` * Audio: :code:`python -m pip install Red-DiscordBot[voice]`
3. Once that has completed, run :code:`redbot-setup` to set up your instance 3. Once that has completed, run :code:`redbot-setup` to set up your instance
* This will set the location where data will be stored, as well as your * This will set the location where data will be stored, as well as your
storage backend and the name of the instance (which will be used for storage backend and the name of the instance (which will be used for
running the bot) running the bot)
4. Once done setting up the instance, run :code:`redbot <your instance name>` to run Red. 4. Once done setting up the instance, run :code:`redbot <your instance name>` to run Red.
It will walk through the initial setup, asking for your token and a prefix It will walk through the initial setup, asking for your token and a prefix

View File

@ -52,27 +52,37 @@ class Downloader:
self._repo_manager = RepoManager(self.conf) self._repo_manager = RepoManager(self.conf)
async def cog_install_path(self): async def cog_install_path(self):
""" """Get the current cog install path.
Returns the current cog install path.
:return: Returns
-------
pathlib.Path
The default cog install path.
""" """
return await self.bot.cog_mgr.install_path() return await self.bot.cog_mgr.install_path()
async def installed_cogs(self) -> Tuple[Installable]: async def installed_cogs(self) -> Tuple[Installable]:
""" """Get info on installed cogs.
Returns the dictionary mapping cog name to install location
and repo name. Returns
:return: -------
`tuple` of `Installable`
All installed cogs / shared lib directories.
""" """
installed = await self.conf.installed() installed = await self.conf.installed()
# noinspection PyTypeChecker # noinspection PyTypeChecker
return tuple(Installable.from_json(v) for v in installed) return tuple(Installable.from_json(v) for v in installed)
async def _add_to_installed(self, cog: Installable): async def _add_to_installed(self, cog: Installable):
""" """Mark a cog as installed.
Marks a cog as installed.
:param cog: Parameters
:return: ----------
cog : Installable
The cog to check off.
""" """
installed = await self.conf.installed() installed = await self.conf.installed()
cog_json = cog.to_json() cog_json = cog.to_json()
@ -82,10 +92,13 @@ class Downloader:
await self.conf.installed.set(installed) await self.conf.installed.set(installed)
async def _remove_from_installed(self, cog: Installable): async def _remove_from_installed(self, cog: Installable):
""" """Remove a cog from the saved list of installed cogs.
Removes a cog from the saved list of installed cogs.
:param cog: Parameters
:return: ----------
cog : Installable
The cog to remove.
""" """
installed = await self.conf.installed() installed = await self.conf.installed()
cog_json = cog.to_json() cog_json = cog.to_json()
@ -326,11 +339,19 @@ class Downloader:
await ctx.send(box(msg)) await ctx.send(box(msg))
async def is_installed(self, cog_name: str) -> (bool, Union[Installable, None]): async def is_installed(self, cog_name: str) -> (bool, Union[Installable, None]):
""" """Check to see if a cog has been installed through Downloader.
Checks to see if a cog with the given name was installed
through Downloader. Parameters
:param cog_name: ----------
:return: is_installed, Installable cog_name : str
The name of the cog to check for.
Returns
-------
`tuple` of (`bool`, `Installable`)
:code:`(True, Installable)` if the cog is installed, else
:code:`(False, None)`.
""" """
for installable in await self.installed_cogs(): for installable in await self.installed_cogs():
if installable.name == cog_name: if installable.name == cog_name:
@ -339,11 +360,20 @@ class Downloader:
def format_findcog_info(self, command_name: str, def format_findcog_info(self, command_name: str,
cog_installable: Union[Installable, object]=None) -> str: cog_installable: Union[Installable, object]=None) -> str:
""" """Format a cog's info for output to discord.
Formats the info for output to discord
:param command_name: Parameters
:param cog_installable: Can be an Installable instance or a Cog instance. ----------
:return: str command_name : str
Name of the command which belongs to the cog.
cog_installable : `Installable` or `object`
Can be an `Installable` instance or a Cog instance.
Returns
-------
str
A formatted message for the user.
""" """
if isinstance(cog_installable, Installable): if isinstance(cog_installable, Installable):
made_by = ", ".join(cog_installable.author) or _("Missing from info.json") made_by = ", ".join(cog_installable.author) or _("Missing from info.json")
@ -360,12 +390,20 @@ class Downloader:
return msg.format(command_name, made_by, repo_url, cog_name) return msg.format(command_name, made_by, repo_url, cog_name)
def cog_name_from_instance(self, instance: object) -> str: def cog_name_from_instance(self, instance: object) -> str:
""" """Determines the cog name that Downloader knows from the cog instance.
Determines the cog name that Downloader knows from the cog instance.
Probably. Probably.
:param instance:
:return: Parameters
----------
instance : object
The cog instance.
Returns
-------
str
The name of the cog according to Downloader..
""" """
splitted = instance.__module__.split('.') splitted = instance.__module__.split('.')
return splitted[-2] return splitted[-2]

View File

@ -16,11 +16,38 @@ class InstallableType(Enum):
class Installable(RepoJSONMixin): class Installable(RepoJSONMixin):
""" """Base class for anything the Downloader cog can install.
Base class for anything the Downloader cog can install.
- Modules - Modules
- Repo Libraries - Repo Libraries
- Other stuff? - Other stuff?
The attributes of this class will mostly come from the installation's
info.json.
Attributes
----------
repo_name : `str`
Name of the repository which this package belongs to.
author : `tuple` of `str`, optional
Name(s) of the author(s).
bot_version : `tuple` of `int`
The minimum bot version required for this installation. Right now
this is always :code:`3.0.0`.
hidden : `bool`
Whether or not this cog will be hidden from the user when they use
`Downloader`'s commands.
required_cogs : `dict`
In the form :code:`{cog_name : repo_url}`, these are cogs which are
required for this installation.
requirements : `tuple` of `str`
Required libraries for this installation.
tags : `tuple` of `str`
List of tags to assist in searching.
type : `int`
The type of this installation, as specified by
:class:`InstallationType`.
""" """
INFO_FILE_DESCRIPTION = """ INFO_FILE_DESCRIPTION = """
@ -28,10 +55,13 @@ class Installable(RepoJSONMixin):
""" """
def __init__(self, location: Path): def __init__(self, location: Path):
""" """Base installable initializer.
Base installable initializer.
Parameters
----------
location : pathlib.Path
Location (file or folder) to the installable.
:param location: Location (file or folder) to the installable.
""" """
super().__init__(location) super().__init__(location)
@ -62,6 +92,7 @@ class Installable(RepoJSONMixin):
@property @property
def name(self): def name(self):
"""`str` : The name of this package."""
return self._location.stem return self._location.stem
async def copy_to(self, target_dir: Path) -> bool: async def copy_to(self, target_dir: Path) -> bool:

View File

@ -164,10 +164,13 @@ class Repo(RepoJSONMixin):
) )
async def clone(self) -> Tuple[str]: async def clone(self) -> Tuple[str]:
""" """Clone a new repo.
Clones a new repo.
Returns
-------
`tuple` of `str`
All available module names from this repo.
:return: List of available module names from this repo.
""" """
exists, path = self._existing_git_repo() exists, path = self._existing_git_repo()
if exists: if exists:
@ -202,10 +205,13 @@ class Repo(RepoJSONMixin):
return self._update_available_modules() return self._update_available_modules()
async def current_branch(self) -> str: async def current_branch(self) -> str:
""" """Determine the current branch using git commands.
Determines the current branch using git commands.
Returns
-------
str
The current branch name.
:return: Current branch name
""" """
exists, _ = self._existing_git_repo() exists, _ = self._existing_git_repo()
if not exists: if not exists:
@ -226,11 +232,18 @@ class Repo(RepoJSONMixin):
return p.stdout.decode().strip() return p.stdout.decode().strip()
async def current_commit(self, branch: str=None) -> str: async def current_commit(self, branch: str=None) -> str:
""" """Determine the current commit hash of the repo.
Determines the current commit hash of the repo.
Parameters
----------
branch : `str`, optional
Override for repo's branch attribute.
Returns
-------
str
The requested commit hash.
:param branch: Override for repo's branch attribute
:return: Commit hash string
""" """
if branch is None: if branch is None:
branch = self.branch branch = self.branch
@ -254,10 +267,13 @@ class Repo(RepoJSONMixin):
return p.stdout.decode().strip() return p.stdout.decode().strip()
async def hard_reset(self, branch: str=None) -> None: async def hard_reset(self, branch: str=None) -> None:
""" """Perform a hard reset on the current repo.
Performs a hard reset on the current repo.
Parameters
----------
branch : `str`, optional
Override for repo branch attribute.
:param branch: Override for repo branch attribute.
""" """
if branch is None: if branch is None:
branch = self.branch branch = self.branch
@ -281,11 +297,13 @@ class Repo(RepoJSONMixin):
" the following path: {}".format(self.folder_path)) " the following path: {}".format(self.folder_path))
async def update(self) -> (str, str): async def update(self) -> (str, str):
""" """Update the current branch of this repo.
Updates the current branch of this repo.
Returns
-------
`tuple` of `str`
:py:code`(old commit hash, new commit hash)`
:return: tuple of (old commit hash, new commit hash)
:rtype: tuple
""" """
curr_branch = await self.current_branch() curr_branch = await self.current_branch()
old_commit = await self.current_commit(branch=curr_branch) old_commit = await self.current_commit(branch=curr_branch)
@ -310,13 +328,20 @@ class Repo(RepoJSONMixin):
return old_commit, new_commit return old_commit, new_commit
async def install_cog(self, cog: Installable, target_dir: Path) -> bool: async def install_cog(self, cog: Installable, target_dir: Path) -> bool:
""" """Install a cog to the target directory.
Copies a cog to the target directory.
Parameters
----------
cog : Installable
The package to install.
target_dir : pathlib.Path
The target directory for the cog installation.
Returns
-------
bool
The success of the installation.
:param Installable cog: Cog to install.
:param pathlib.Path target_dir: Directory to install the cog in.
:return: Installation success status.
:rtype: bool
""" """
if cog not in self.available_cogs: if cog not in self.available_cogs:
raise DownloaderException("That cog does not exist in this repo") raise DownloaderException("That cog does not exist in this repo")
@ -330,14 +355,23 @@ class Repo(RepoJSONMixin):
return await cog.copy_to(target_dir=target_dir) return await cog.copy_to(target_dir=target_dir)
async def install_libraries(self, target_dir: Path, libraries: Tuple[Installable]=()) -> bool: async def install_libraries(self, target_dir: Path, libraries: Tuple[Installable]=()) -> bool:
""" """Install shared libraries to the target directory.
Copies all shared libraries (or a given subset) to the target
directory. If :code:`libraries` is not specified, all shared libraries in the repo
will be installed.
Parameters
----------
target_dir : pathlib.Path
Directory to install shared libraries to.
libraries : `tuple` of `Installable`
A subset of available libraries.
Returns
-------
bool
The success of the installation.
:param pathlib.Path target_dir: Directory to install shared libraries to.
:param tuple(Installable) libraries: A subset of available libraries.
:return: Status of all installs.
:rtype: bool
""" """
if libraries: if libraries:
if not all([i in self.available_libraries for i in libraries]): if not all([i in self.available_libraries for i in libraries]):
@ -350,15 +384,23 @@ class Repo(RepoJSONMixin):
return True return True
async def install_requirements(self, cog: Installable, target_dir: Path) -> bool: async def install_requirements(self, cog: Installable, target_dir: Path) -> bool:
""" """Install a cog's requirements.
Installs the requirements defined by the requirements
attribute on the cog object and puts them in the given Requirements will be installed via pip directly into
target directory. :code:`target_dir`.
Parameters
----------
cog : Installable
Cog for which to install requirements.
target_dir : pathlib.Path
Path to directory where requirements are to be installed.
Returns
-------
bool
Success of the installation.
:param Installable cog: Cog for which to install requirements.
:param pathlib.Path target_dir: Path to which to install requirements.
:return: Status of requirements install.
:rtype: bool
""" """
if not target_dir.is_dir(): if not target_dir.is_dir():
raise ValueError("Target directory is not a directory.") raise ValueError("Target directory is not a directory.")
@ -367,14 +409,20 @@ class Repo(RepoJSONMixin):
return await self.install_raw_requirements(cog.requirements, target_dir) return await self.install_raw_requirements(cog.requirements, target_dir)
async def install_raw_requirements(self, requirements: Tuple[str], target_dir: Path) -> bool: async def install_raw_requirements(self, requirements: Tuple[str], target_dir: Path) -> bool:
""" """Install a list of requirements using pip.
Installs a list of requirements using pip and places them into
the given target directory. Parameters
----------
requirements : `tuple` of `str`
List of requirement names to install via pip.
target_dir : pathlib.Path
Path to directory where requirements are to be installed.
Returns
-------
bool
Success of the installation
:param tuple(str) requirements: List of requirement names to install via pip.
:param pathlib.Path target_dir: Directory to install requirements to.
:return: Status of all requirements install.
:rtype: bool
""" """
if len(requirements) == 0: if len(requirements) == 0:
return True return True
@ -398,10 +446,9 @@ class Repo(RepoJSONMixin):
@property @property
def available_cogs(self) -> Tuple[Installable]: def available_cogs(self) -> Tuple[Installable]:
""" """`tuple` of `installable` : All available cogs in this Repo.
Returns a list of available cogs (not shared libraries and not hidden).
:rtype: tuple(Installable) This excludes hidden or shared packages.
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
return tuple( return tuple(
@ -411,10 +458,8 @@ class Repo(RepoJSONMixin):
@property @property
def available_libraries(self) -> Tuple[Installable]: def available_libraries(self) -> Tuple[Installable]:
""" """`tuple` of `installable` : All available shared libraries in this
Returns a list of available shared libraries in this repo. Repo.
:rtype: tuple(Installable)
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
return tuple( return tuple(
@ -463,14 +508,22 @@ class RepoManager:
return name.lower() return name.lower()
async def add_repo(self, url: str, name: str, branch: str="master") -> Repo: async def add_repo(self, url: str, name: str, branch: str="master") -> Repo:
""" """Add and clone a git repository.
Adds a repo and clones it.
Parameters
----------
url : str
URL to the git repository.
name : str
Internal name of the repository.
branch : str
Name of the default branch to checkout into.
Returns
-------
Repo
New Repo object representing the cloned repository.
:param url: URL of git repo to clone.
:param name: Internal name of repo.
:param branch: Branch to clone.
:return: New repo object representing cloned repo.
:rtype: Repo
""" """
name = self.validate_and_normalize_repo_name(name) name = self.validate_and_normalize_repo_name(name)
if self.does_repo_exist(name): if self.does_repo_exist(name):
@ -490,30 +543,45 @@ class RepoManager:
return r return r
def get_repo(self, name: str) -> Union[Repo, None]: def get_repo(self, name: str) -> Union[Repo, None]:
""" """Get a Repo object for a repository.
Returns a repo object with the given name.
Parameters
----------
name : str
The name of the repository to retrieve.
Returns
-------
`Repo` or `None`
Repo object for the repository, if it exists.
:param name: Repo name
:return: Repo object or ``None`` if repo does not exist.
:rtype: Union[Repo, None]
""" """
return self._repos.get(name, None) return self._repos.get(name, None)
def get_all_repo_names(self) -> Tuple[str]: def get_all_repo_names(self) -> Tuple[str]:
""" """Get all repo names.
Returns a tuple of all repo names
Returns
-------
`tuple` of `str`
:rtype: tuple(str)
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
return tuple(self._repos.keys()) return tuple(self._repos.keys())
async def delete_repo(self, name: str): async def delete_repo(self, name: str):
""" """Delete a repository and its folders.
Deletes a repo and its folders with the given name.
Parameters
----------
name : str
The name of the repository to delete.
Raises
------
MissingGitRepo
If the repo does not exist.
:param name: Name of the repo to delete.
:raises MissingGitRepo: If the repo does not exist.
""" """
repo = self.get_repo(name) repo = self.get_repo(name)
if repo is None: if repo is None:
@ -529,12 +597,14 @@ class RepoManager:
await self._save_repos() await self._save_repos()
async def update_all_repos(self) -> MutableMapping[Repo, Tuple[str, str]]: async def update_all_repos(self) -> MutableMapping[Repo, Tuple[str, str]]:
""" """Call `Repo.update` on all repositories.
Calls :py:meth:`Repo.update` on all repos.
Returns
-------
dict
A mapping of `Repo` objects that received new commits to a `tuple`
of `str` containing old and new commit hashes.
:return:
A mapping of :py:class:`Repo` objects that received new commits to a tuple containing old and
new commit hashes.
""" """
ret = {} ret = {}
for _, repo in self._repos.items(): for _, repo in self._repos.items():

View File

@ -37,7 +37,9 @@ _bank_type = type("Bank", (object,), {})
class Account: class Account:
"""A single account. This class should ONLY be instantiated by the bank itself.""" """A single account.
This class should ONLY be instantiated by the bank itself."""
def __init__(self, name: str, balance: int, created_at: datetime.datetime): def __init__(self, name: str, balance: int, created_at: datetime.datetime):
self.name = name self.name = name
@ -57,60 +59,87 @@ if not os.environ.get('BUILDING_DOCS'):
def _encoded_current_time() -> int: def _encoded_current_time() -> int:
""" """Get the current UTC time as a timestamp.
Encoded current timestamp in UTC.
:return: Returns
-------
int
The current UTC timestamp.
""" """
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
return _encode_time(now) return _encode_time(now)
def _encode_time(time: datetime.datetime) -> int: def _encode_time(time: datetime.datetime) -> int:
""" """Convert a datetime object to a serializable int.
Goes from datetime object to serializable int.
:param time: Parameters
:return: ----------
time : datetime.datetime
The datetime to convert.
Returns
-------
int
The timestamp of the datetime object.
""" """
ret = int(time.timestamp()) ret = int(time.timestamp())
return ret return ret
def _decode_time(time: int) -> datetime.datetime: def _decode_time(time: int) -> datetime.datetime:
""" """Convert a timestamp to a datetime object.
Returns decoded timestamp in UTC.
:param time: Parameters
:return: ----------
time : int
The timestamp to decode.
Returns
-------
datetime.datetime
The datetime object from the timestamp.
""" """
return datetime.datetime.utcfromtimestamp(time) return datetime.datetime.utcfromtimestamp(time)
async def get_balance(member: discord.Member) -> int: async def get_balance(member: discord.Member) -> int:
""" """Get the current balance of a member.
Gets the current balance of a member.
:param discord.Member member: Parameters
----------
member : discord.Member
The member whose balance to check. The member whose balance to check.
:return:
The member's balance Returns
:rtype: -------
int int
The member's balance
""" """
acc = await get_account(member) acc = await get_account(member)
return acc.balance return acc.balance
async def can_spend(member: discord.Member, amount: int) -> bool: async def can_spend(member: discord.Member, amount: int) -> bool:
""" """Determine if a member can spend the given amount.
Determines if a member can spend the given amount.
:param discord.Member member: Parameters
----------
member : discord.Member
The member wanting to spend. The member wanting to spend.
:param int amount: amount : int
The amount the member wants to spend. The amount the member wants to spend.
:return:
:code:`True` if the member has a sufficient balance to spend the amount, else :code:`False`. Returns
:rtype: -------
bool bool
:code:`True` if the member has a sufficient balance to spend the
amount, else :code:`False`.
""" """
if _invalid_amount(amount): if _invalid_amount(amount):
return False return False
@ -118,21 +147,25 @@ async def can_spend(member: discord.Member, amount: int) -> bool:
async def set_balance(member: discord.Member, amount: int) -> int: async def set_balance(member: discord.Member, amount: int) -> int:
""" """Set an account balance.
Sets an account balance.
May raise ValueError if amount is invalid. Parameters
----------
:param discord.Member member: member : discord.Member
The member whose balance to set. The member whose balance to set.
:param int amount: amount : int
The amount to set the balance to. The amount to set the balance to.
:return:
New account balance. Returns
:rtype: -------
int int
:raises ValueError: New account balance.
If attempting to set the balance to a negative number
Raises
------
ValueError
If attempting to set the balance to a negative number.
""" """
if amount < 0: if amount < 0:
raise ValueError("Not allowed to have negative balance.") raise ValueError("Not allowed to have negative balance.")
@ -157,22 +190,26 @@ def _invalid_amount(amount: int) -> bool:
async def withdraw_credits(member: discord.Member, amount: int) -> int: async def withdraw_credits(member: discord.Member, amount: int) -> int:
""" """Remove a certain amount of credits from an account.
Removes a certain amount of credits from an account.
May raise ValueError if the amount is invalid or if the account has Parameters
insufficient funds. ----------
member : discord.Member
:param discord.Member member:
The member to withdraw credits from. The member to withdraw credits from.
:param int amount: amount : int
The amount to withdraw. The amount to withdraw.
:return:
New account balance. Returns
:rtype: -------
int int
:raises ValueError: New account balance.
if the withdrawal amount is invalid or if the account has insufficient funds
Raises
------
ValueError
If the withdrawal amount is invalid or if the account has insufficient
funds.
""" """
if _invalid_amount(amount): if _invalid_amount(amount):
raise ValueError("Invalid withdrawal amount {} <= 0".format(amount)) raise ValueError("Invalid withdrawal amount {} <= 0".format(amount))
@ -185,21 +222,25 @@ async def withdraw_credits(member: discord.Member, amount: int) -> int:
async def deposit_credits(member: discord.Member, amount: int) -> int: async def deposit_credits(member: discord.Member, amount: int) -> int:
""" """Add a given amount of credits to an account.
Adds a given amount of credits to an account.
May raise ValueError if the amount is invalid. Parameters
----------
:param discord.Member member: member : discord.Member
The member to deposit credits to. The member to deposit credits to.
:param int amount: amount : int
The amount to deposit. The amount to deposit.
:return:
The new balance Returns
:rtype: -------
int int
:raises ValueError: The new balance.
Raises
------
ValueError
If the deposit amount is invalid. If the deposit amount is invalid.
""" """
if _invalid_amount(amount): if _invalid_amount(amount):
raise ValueError("Invalid withdrawal amount {} <= 0".format(amount)) raise ValueError("Invalid withdrawal amount {} <= 0".format(amount))
@ -209,24 +250,27 @@ async def deposit_credits(member: discord.Member, amount: int) -> int:
async def transfer_credits(from_: discord.Member, to: discord.Member, amount: int): async def transfer_credits(from_: discord.Member, to: discord.Member, amount: int):
""" """Transfer a given amount of credits from one account to another.
Transfers a given amount of credits from one account to another.
May raise ValueError if the amount is invalid or if the :code:`from_` Parameters
account has insufficient funds. ----------
from_: discord.Member
:param discord.Member from_:
The member to transfer from. The member to transfer from.
:param discord.Member to: to : discord.Member
The member to transfer to. The member to transfer to.
:param int amount: amount : int
The amount to transfer. The amount to transfer.
:return:
The new balance. Returns
:rtype: -------
int int
:raises ValueError: The new balance.
If the amount is invalid or if :code:`from_` has insufficient funds.
Raises
------
ValueError
If the amount is invalid or if ``from_`` has insufficient funds.
""" """
if _invalid_amount(amount): if _invalid_amount(amount):
raise ValueError("Invalid transfer amount {} <= 0".format(amount)) raise ValueError("Invalid transfer amount {} <= 0".format(amount))
@ -236,18 +280,18 @@ async def transfer_credits(from_: discord.Member, to: discord.Member, amount: in
async def wipe_bank(user: Union[discord.User, discord.Member]): async def wipe_bank(user: Union[discord.User, discord.Member]):
""" """Delete all accounts from the bank.
Deletes all accounts from the bank.
.. important:: .. important::
A member is required if the bank is currently guild specific. A member is required if the bank is currently guild specific.
:param user: Parameters
----------
user : `discord.User` or `discord.Member`
A user to be used in clearing the bank, this is required for technical A user to be used in clearing the bank, this is required for technical
reasons and it does not matter which user/member is used. reasons and it does not matter which user/member is used.
:type user:
discord.User or discord.Member
""" """
if await is_global(): if await is_global():
await _conf.user(user).clear() await _conf.user(user).clear()
@ -256,19 +300,23 @@ async def wipe_bank(user: Union[discord.User, discord.Member]):
async def get_guild_accounts(guild: discord.Guild) -> List[Account]: async def get_guild_accounts(guild: discord.Guild) -> List[Account]:
""" """Get all account data for the given guild.
Gets all account data for the given guild.
May raise RuntimeError if the bank is currently global. Parameters
----------
:param discord.Guild guild: guild : discord.Guild
The guild to get accounts for. The guild to get accounts for.
:return:
A generator for all guild accounts. Returns
:rtype: -------
generator `list` of `Account`
:raises RuntimeError: A list of all guild accounts.
If the bank is global.
Raises
------
RuntimeError
If the bank is currently global.
""" """
if is_global(): if is_global():
raise RuntimeError("The bank is currently global.") raise RuntimeError("The bank is currently global.")
@ -283,19 +331,23 @@ async def get_guild_accounts(guild: discord.Guild) -> List[Account]:
async def get_global_accounts(user: discord.User) -> List[Account]: async def get_global_accounts(user: discord.User) -> List[Account]:
""" """Get all global account data.
Gets all global account data.
May raise RuntimeError if the bank is currently guild specific. Parameters
----------
:param discord.User user: user : discord.User
A user to be used for getting accounts. A user to be used for getting accounts.
:return:
A generator of all global accounts. Returns
:rtype: -------
generator `list` of `Account`
:raises RuntimeError: A list of all global accounts.
If the bank is guild specific.
Raises
------
RuntimeError
If the bank is currently guild specific.
""" """
if not is_global(): if not is_global():
raise RuntimeError("The bank is not currently global.") raise RuntimeError("The bank is not currently global.")
@ -311,18 +363,20 @@ async def get_global_accounts(user: discord.User) -> List[Account]:
async def get_account(member: Union[discord.Member, discord.User]) -> Account: async def get_account(member: Union[discord.Member, discord.User]) -> Account:
""" """Get the appropriate account for the given user or member.
Gets the appropriate account for the given user or member. A member is
required if the bank is currently guild specific.
:param member: A member is required if the bank is currently guild specific.
Parameters
----------
member : `discord.User` or `discord.Member`
The user whose account to get. The user whose account to get.
:type member:
discord.User or discord.Member Returns
:return: -------
Account
The user's account. The user's account.
:rtype:
:py:class:`Account`
""" """
if await is_global(): if await is_global():
acc_data = (await _conf.user(member)()).copy() acc_data = (await _conf.user(member)()).copy()
@ -344,36 +398,43 @@ async def get_account(member: Union[discord.Member, discord.User]) -> Account:
async def is_global() -> bool: async def is_global() -> bool:
""" """Determine if the bank is currently global.
Determines if the bank is currently global.
:return: Returns
:code:`True` if the bank is global, otherwise :code:`False`. -------
:rtype:
bool bool
:code:`True` if the bank is global, otherwise :code:`False`.
""" """
return await _conf.is_global() return await _conf.is_global()
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.
Sets global status of the bank, requires the user parameter for technical reasons.
Requires the user parameter for technical reasons.
.. important:: .. important::
All accounts are reset when you switch! All accounts are reset when you switch!
:param global_: Parameters
----------
global_ : bool
:code:`True` will set bank to global mode. :code:`True` will set bank to global mode.
:param user: user : `discord.User` or `discord.Member`
Must be a Member object if changing TO global mode. Must be a Member object if changing TO global mode.
:type user:
discord.User or discord.Member Returns
:return: -------
New bank mode, :code:`True` is global.
:rtype:
bool bool
:raises RuntimeError: New bank mode, :code:`True` is global.
If bank is becoming global and :py:class:`discord.Member` was not provided.
Raises
------
RuntimeError
If bank is becoming global and a `discord.Member` was not provided.
""" """
if (await is_global()) is global_: if (await is_global()) is global_:
return global_ return global_
@ -391,20 +452,24 @@ async def set_global(global_: bool, user: Union[discord.User, discord.Member]) -
async def get_bank_name(guild: discord.Guild=None) -> str: async def get_bank_name(guild: discord.Guild=None) -> str:
""" """Get the current bank name.
Gets the current bank name. If the bank is guild-specific the
guild parameter is required.
May raise RuntimeError if guild is missing and required. Parameters
----------
guild : `discord.Guild`, optional
The guild to get the bank name for (required if bank is
guild-specific).
:param discord.Guild guild: Returns
The guild to get the bank name for (required if bank is guild-specific). -------
:return:
The bank's name.
:rtype:
str str
:raises RuntimeError: The bank's name.
Raises
------
RuntimeError
If the bank is guild-specific and guild was not provided. If the bank is guild-specific and guild was not provided.
""" """
if await is_global(): if await is_global():
return await _conf.bank_name() return await _conf.bank_name()
@ -415,22 +480,26 @@ async def get_bank_name(guild: discord.Guild=None) -> str:
async def set_bank_name(name: str, guild: discord.Guild=None) -> str: async def set_bank_name(name: str, guild: discord.Guild=None) -> str:
""" """Set the bank name.
Sets the bank name, if bank is guild specific the guild parameter is
required.
May throw RuntimeError if guild is required and missing. Parameters
----------
name : str
The new name for the bank.
guild : `discord.Guild`, optional
The guild to set the bank name for (required if bank is
guild-specific).
:param str name: Returns
The new name for the bank. -------
:param discord.Guild guild:
The guild to set the bank name for (required if bank is guild-specific).
:return:
The new name for the bank.
:rtype:
str str
:raises RuntimeError: The new name for the bank.
Raises
------
RuntimeError
If the bank is guild-specific and guild was not provided. If the bank is guild-specific and guild was not provided.
""" """
if await is_global(): if await is_global():
await _conf.bank_name.set(name) await _conf.bank_name.set(name)
@ -443,20 +512,24 @@ async def set_bank_name(name: str, guild: discord.Guild=None) -> str:
async def get_currency_name(guild: discord.Guild=None) -> str: async def get_currency_name(guild: discord.Guild=None) -> str:
""" """Get the currency name of the bank.
Gets the currency name of the bank. The guild parameter is required if
the bank is guild-specific.
May raise RuntimeError if the guild is missing and required. Parameters
----------
guild : `discord.Guild`, optional
The guild to get the currency name for (required if bank is
guild-specific).
:param discord.Guild guild: Returns
The guild to get the currency name for (required if bank is guild-specific). -------
:return:
The currency name.
:rtype:
str str
:raises RuntimeError: The currency name.
Raises
------
RuntimeError
If the bank is guild-specific and guild was not provided. If the bank is guild-specific and guild was not provided.
""" """
if await is_global(): if await is_global():
return await _conf.currency() return await _conf.currency()
@ -467,22 +540,26 @@ async def get_currency_name(guild: discord.Guild=None) -> str:
async def set_currency_name(name: str, guild: discord.Guild=None) -> str: async def set_currency_name(name: str, guild: discord.Guild=None) -> str:
""" """Set the currency name for the bank.
Sets the currency name for the bank, if bank is guild specific the
guild parameter is required.
May raise RuntimeError if guild is missing and required. Parameters
----------
name : str
The new name for the currency.
guild : `discord.Guild`, optional
The guild to set the currency name for (required if bank is
guild-specific).
:param str name: Returns
The new name for the currency. -------
:param discord.Guild guild:
The guild to set the currency name for (required if bank is guild-specific).
:return:
The new name for the currency.
:rtype:
str str
:raises RuntimeError: The new name for the currency.
Raises
------
RuntimeError
If the bank is guild-specific and guild was not provided. If the bank is guild-specific and guild was not provided.
""" """
if await is_global(): if await is_global():
await _conf.currency.set(name) await _conf.currency.set(name)
@ -495,20 +572,24 @@ async def set_currency_name(name: str, guild: discord.Guild=None) -> str:
async def get_default_balance(guild: discord.Guild=None) -> int: async def get_default_balance(guild: discord.Guild=None) -> int:
""" """Get the current default balance amount.
Gets the current default balance amount. If the bank is guild-specific
you must pass guild.
May raise RuntimeError if guild is missing and required. Parameters
----------
guild : `discord.Guild`, optional
The guild to get the default balance for (required if bank is
guild-specific).
:param discord.Guild guild: Returns
The guild to get the default balance for (required if bank is guild-specific). -------
:return: int
The bank's default balance. The bank's default balance.
:rtype:
str Raises
:raises RuntimeError: ------
RuntimeError
If the bank is guild-specific and guild was not provided. If the bank is guild-specific and guild was not provided.
""" """
if await is_global(): if await is_global():
return await _conf.default_balance() return await _conf.default_balance()
@ -519,26 +600,28 @@ async def get_default_balance(guild: discord.Guild=None) -> int:
async def set_default_balance(amount: int, guild: discord.Guild=None) -> int: async def set_default_balance(amount: int, guild: discord.Guild=None) -> int:
""" """Set the default balance amount.
Sets the default balance amount. Guild is required if the bank is
guild-specific.
May raise RuntimeError if guild is missing and required. Parameters
----------
May raise ValueError if amount is invalid. amount : int
:param int amount:
The new default balance. The new default balance.
:param discord.Guild guild: guild : `discord.Guild`, optional
The guild to set the default balance for (required if bank is guild-specific). The guild to set the default balance for (required if bank is
:return: guild-specific).
Returns
-------
int
The new default balance. The new default balance.
:rtype:
str Raises
:raises RuntimeError: ------
RuntimeError
If the bank is guild-specific and guild was not provided. If the bank is guild-specific and guild was not provided.
:raises ValueError: ValueError
If the amount is invalid. If the amount is invalid.
""" """
amount = int(amount) amount = int(amount)
if amount < 0: if amount < 0:

View File

@ -14,6 +14,14 @@ from . import Config, i18n, RedContext
class RedBase(BotBase): class RedBase(BotBase):
"""Mixin for the main bot class.
This exists because `Red` inherits from `discord.AutoShardedClient`, which
is something other bot classes (namely selfbots) may not want to have as
a parent class.
Selfbots should inherit from this mixin along with `discord.Client`.
"""
def __init__(self, cli_flags, bot_dir: Path=Path.cwd(), **kwargs): def __init__(self, cli_flags, bot_dir: Path=Path.cwd(), **kwargs):
self._shutdown_mode = ExitCodes.CRITICAL self._shutdown_mode = ExitCodes.CRITICAL
self.db = Config.get_core_conf(force_registration=True) self.db = Config.get_core_conf(force_registration=True)
@ -168,11 +176,18 @@ class Red(RedBase, discord.AutoShardedClient):
""" """
You're welcome Caleb. You're welcome Caleb.
""" """
async def shutdown(self, *, restart=False): async def shutdown(self, *, restart: bool=False):
"""Gracefully quits Red with exit code 0 """Gracefully quit Red.
If restart is True, the exit code will be 26 instead The program will exit with code :code:`0` by default.
Upon receiving that exit code, the launcher restarts Red"""
Parameters
----------
restart : bool
If :code:`True`, the program will exit with code :code:`26`. If the
launcher sees this, it will attempt to restart the bot.
"""
if not restart: if not restart:
self._shutdown_mode = ExitCodes.SHUTDOWN self._shutdown_mode = ExitCodes.SHUTDOWN
else: else:

View File

@ -16,10 +16,12 @@ __all__ = ["CogManager"]
class CogManager: class CogManager:
""" """Directory manager for Red's cogs.
This module allows you to load cogs from multiple directories and even from outside the bot
directory. You may also set a directory for downloader to install new cogs to, the default This module allows you to load cogs from multiple directories and even from
being the :code:`cogs/` folder in the root bot directory. outside the bot directory. You may also set a directory for downloader to
install new cogs to, the default being the :code:`cogs/` folder in the root
bot directory.
""" """
def __init__(self, paths: Tuple[str]=()): def __init__(self, paths: Tuple[str]=()):
self.conf = Config.get_conf(self, 2938473984732, True) self.conf = Config.get_conf(self, 2938473984732, True)
@ -33,8 +35,13 @@ class CogManager:
self._paths = list(paths) self._paths = list(paths)
async def paths(self) -> Tuple[Path, ...]: async def paths(self) -> Tuple[Path, ...]:
""" """Get all currently valid path directories.
All currently valid path directories.
Returns
-------
`tuple` of `pathlib.Path`
All valid cog paths.
""" """
conf_paths = await self.conf.paths() conf_paths = await self.conf.paths()
other_paths = self._paths other_paths = self._paths
@ -47,26 +54,40 @@ class CogManager:
return tuple(p.resolve() for p in paths if p.is_dir()) return tuple(p.resolve() for p in paths if p.is_dir())
async def install_path(self) -> Path: async def install_path(self) -> Path:
""" """Get the install path for 3rd party cogs.
The install path for 3rd party cogs.
Returns
-------
pathlib.Path
The path to the directory where 3rd party cogs are stored.
""" """
p = Path(await self.conf.install_path()) p = Path(await self.conf.install_path())
return p.resolve() return p.resolve()
async def set_install_path(self, path: Path) -> Path: async def set_install_path(self, path: Path) -> Path:
""" """Set the install path for 3rd party cogs.
Install path setter, will return the absolute path to
the given path.
.. note::
Note
----
The bot will not remember your old cog install path which means The bot will not remember your old cog install path which means
that ALL PREVIOUSLY INSTALLED COGS will now be unfindable. that **all previously installed cogs** will no longer be found.
:param pathlib.Path path: Parameters
----------
path : pathlib.Path
The new directory for cog installs. The new directory for cog installs.
:raises ValueError:
Returns
-------
pathlib.Path
Absolute path to the new install directory.
Raises
------
ValueError
If :code:`path` is not an existing directory. If :code:`path` is not an existing directory.
""" """
if not path.is_dir(): if not path.is_dir():
raise ValueError("The install path must be an existing directory.") raise ValueError("The install path must be an existing directory.")
@ -76,14 +97,16 @@ class CogManager:
@staticmethod @staticmethod
def _ensure_path_obj(path: Union[Path, str]) -> Path: def _ensure_path_obj(path: Union[Path, str]) -> Path:
""" """Guarantee an object will be a path object.
Guarantees an object will be a path object.
:param path: Parameters
:type path: ----------
pathlib.Path or str path : `pathlib.Path` or `str`
:rtype:
Returns
-------
pathlib.Path pathlib.Path
""" """
try: try:
path.exists() path.exists()
@ -92,17 +115,21 @@ class CogManager:
return path return path
async def add_path(self, path: Union[Path, str]): async def add_path(self, path: Union[Path, str]):
""" """Add a cog path to current list.
Adds a cog path to current list, will ignore duplicates. Does have
a side effect of removing all invalid paths from the saved path
list.
:param path: This will ignore duplicates. Does have a side effect of removing all
invalid paths from the saved path list.
Parameters
----------
path : `pathlib.Path` or `str`
Path to add. Path to add.
:type path:
pathlib.Path or str Raises
:raises ValueError: ------
ValueError
If :code:`path` does not resolve to an existing directory. If :code:`path` does not resolve to an existing directory.
""" """
path = self._ensure_path_obj(path) path = self._ensure_path_obj(path)
@ -121,15 +148,18 @@ class CogManager:
await self.set_paths(all_paths) await self.set_paths(all_paths)
async def remove_path(self, path: Union[Path, str]) -> Tuple[Path, ...]: async def remove_path(self, path: Union[Path, str]) -> Tuple[Path, ...]:
""" """Remove a path from the current paths list.
Removes a path from the current paths list.
:param path: Path to remove. Parameters
:type path: ----------
pathlib.Path or str path : `pathlib.Path` or `str`
:return: Path to remove.
Returns
-------
`tuple` of `pathlib.Path`
Tuple of new valid paths. Tuple of new valid paths.
:rtype: tuple
""" """
path = self._ensure_path_obj(path) path = self._ensure_path_obj(path)
all_paths = list(await self.paths()) all_paths = list(await self.paths())
@ -139,27 +169,35 @@ class CogManager:
return tuple(all_paths) return tuple(all_paths)
async def set_paths(self, paths_: List[Path]): async def set_paths(self, paths_: List[Path]):
""" """Set the current paths list.
Sets the current paths list.
:param List[pathlib.Path] paths_: Parameters
----------
paths_ : `list` of `pathlib.Path`
List of paths to set. List of paths to set.
""" """
str_paths = [str(p) for p in paths_] str_paths = [str(p) for p in paths_]
await self.conf.paths.set(str_paths) await self.conf.paths.set(str_paths)
async def find_cog(self, name: str) -> ModuleSpec: async def find_cog(self, name: str) -> ModuleSpec:
""" """Find a cog in the list of available paths.
Finds a cog in the list of available paths.
:param name: Parameters
----------
name : str
Name of the cog to find. Name of the cog to find.
:raises RuntimeError:
If there is no cog with the given name. Returns
:return: -------
A module spec to be used for specialized cog loading.
:rtype:
importlib.machinery.ModuleSpec importlib.machinery.ModuleSpec
A module spec to be used for specialized cog loading.
Raises
------
RuntimeError
If there is no cog with the given name.
""" """
resolved_paths = [str(p.resolve()) for p in await self.paths()] resolved_paths = [str(p.resolve()) for p in await self.paths()]
for finder, module_name, _ in pkgutil.iter_modules(resolved_paths): for finder, module_name, _ in pkgutil.iter_modules(resolved_paths):
@ -173,11 +211,10 @@ class CogManager:
@staticmethod @staticmethod
def invalidate_caches(): def invalidate_caches():
""" """Re-evaluate modules in the py cache.
This is an alias for an importlib internal and should be called This is an alias for an importlib internal and should be called
any time that a new module has been installed to a cog directory. any time that a new module has been installed to a cog directory.
*I think.*
""" """
invalidate_caches() invalidate_caches()

View File

@ -11,20 +11,18 @@ log = logging.getLogger("red.config")
class Value: class Value:
""" """A singular "value" of data.
A singular "value" of data.
.. py:attribute:: identifiers Attributes
----------
identifiers : `tuple` of `str`
This attribute provides all the keys necessary to get a specific data
element from a json document.
default
The default value for the data element that `identifiers` points at.
spawner : `redbot.core.drivers.red_base.BaseDriver`
A reference to `Config.spawner`.
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
@ -45,12 +43,15 @@ class Value:
return ret return ret
def __call__(self, default=None): def __call__(self, default=None):
""" """Get the literal value of this data element.
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:: Each `Value` object is created by the `Group.__getattr__` method. The
"real" data of the `Value` object is accessed by this method. It is a
replacement for a :code:`get()` method.
Example
-------
::
foo = await conf.guild(some_guild).foo() foo = await conf.guild(some_guild).foo()
@ -64,26 +65,39 @@ class Value:
This is now, for all intents and purposes, a coroutine. This is now, for all intents and purposes, a coroutine.
:param default: Parameters
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`. default : `object`, optional
:type default: Optional[object] This argument acts as an override for the registered default
:return: provided by `default`. This argument is ignored if its
value is :code:`None`.
Returns
-------
types.coroutine
A coroutine object that must be awaited. A coroutine object that must be awaited.
""" """
return self._get(default) return self._get(default)
async def set(self, value): async def set(self, value):
""" """Set the value of the data elements pointed to by `identifiers`.
Sets the value of the data element indicate by :py:attr:`identifiers`.
For example:: Example
-------
::
# Sets global value "foo" to False # Sets global value "foo" to False
await conf.foo.set(False) await conf.foo.set(False)
# Sets guild specific value of "bar" to True # Sets guild specific value of "bar" to True
await conf.guild(some_guild).bar.set(True) await conf.guild(some_guild).bar.set(True)
Parameters
----------
value
The new literal value of this attribute.
""" """
driver = self.spawner.get_driver() driver = self.spawner.get_driver()
await driver.set(self.identifiers, value) await driver.set(self.identifiers, value)
@ -91,16 +105,20 @@ class 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 Represents a group of data, composed of more `Group` or `Value` objects.
in :py:class:`.Value` are also available when working with a :py:class:`.Group` object.
.. py:attribute:: defaults Inherits from `Value` which means that all of the attributes and methods
available in `Value` are also available when working with a `Group` object.
A dictionary of registered default values for this :py:class:`Group`. Attributes
----------
defaults : `dict`
All registered default values for this Group.
force_registration : `bool`
Same as `Config.force_registration`.
spawner : `redbot.core.drivers.red_base.BaseDriver`
A reference to `Config.spawner`.
.. 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,
@ -118,18 +136,28 @@ 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.
Takes in the next accessible item.
1. If it's found to be a group of data we return another :py:class:`Group` object. This special method is called whenever dot notation is used on this
2. If it's found to be a data value we return a :py:class:`.Value` object. object.
3. If it is not found and :py:attr:`force_registration` is :python:`True` then we raise
:py:exc:`AttributeError`. Parameters
4. Otherwise return a :py:class:`.Value` object. ----------
item : str
The name of the attribute being accessed.
Returns
-------
`Group` or `Value`
A child value of this Group. This, of course, can be another
`Group`, due to Config's composite pattern.
Raises
------
AttributeError
If the attribute has not been registered and `force_registration`
is set to :code:`True`.
: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)
@ -170,21 +198,27 @@ class Group(Value):
return super_group return super_group
def is_group(self, item: str) -> bool: def is_group(self, item: str) -> bool:
""" """A helper method for `__getattr__`. Most developers will have no need
A helper method for :py:meth:`__getattr__`. Most developers will have no need to use this. to use this.
Parameters
----------
item : str
See `__getattr__`.
: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:
""" """A helper method for `__getattr__`. Most developers will have no need
A helper method for :py:meth:`__getattr__`. Most developers will have no need to use this. to use this.
Parameters
----------
item : str
See `__getattr__`.
:param str item:
See :py:meth:`__getattr__`.
""" """
try: try:
default = self._defaults[item] default = self._defaults[item]
@ -194,14 +228,18 @@ class Group(Value):
return not isinstance(default, dict) return not isinstance(default, dict)
def get_attr(self, item: str, default=None, resolve=True): def get_attr(self, item: str, default=None, resolve=True):
""" """Manually get an attribute of this Group.
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:: 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. Use of this method should be avoided wherever possible.
Example
-------
A possible use case:: A possible use case::
@commands.command() @commands.command()
@ -211,16 +249,24 @@ class Group(Value):
# Where the value of item is the name of the data field in Config # Where the value of item is the name of the data field in Config
await ctx.send(await self.conf.user(user).get_attr(item)) await ctx.send(await self.conf.user(user).get_attr(item))
:param str item: Parameters
The name of the data field in :py:class:`.Config`. ----------
:param default: item : str
This is an optional override to the registered default for this item. The name of the data field in `Config`.
:param resolve: default
If this is :code:`True` this function will return a coroutine that resolves to a "real" data value, This is an optional override to the registered default for this
if :code:`False` this function will return an instance of :py:class:`Group` or :py:class:`Value` item.
depending on the type of the "real" data value. resolve : bool
:rtype: If this is :code:`True` this function will return a coroutine that
Coroutine or Value resolves to a "real" data value when awaited. If :code:`False`,
this method acts the same as `__getattr__`.
Returns
-------
`types.coroutine` or `Value` or `Group`
The attribute which was requested, its type depending on the value
of :code:`resolve`.
""" """
value = getattr(self, item) value = getattr(self, item)
if resolve: if resolve:
@ -229,36 +275,42 @@ class Group(Value):
return value return value
async def all(self) -> dict: async def all(self) -> dict:
""" """Get a dictionary representation of this group's data.
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.
.. note:: Note
----
The return value of this method will include registered defaults for
values which have not yet been set.
Any values that have not been set from the registered defaults will have their default values Returns
added to the dictionary that this method returns. -------
dict
All of this Group's attributes, resolved as raw data values.
:rtype: dict
""" """
defaults = self.defaults defaults = self.defaults
defaults.update(await self()) defaults.update(await self())
return defaults return defaults
async def all_from_kind(self) -> dict: async def all_from_kind(self) -> dict:
""" """Get all data from this group and its siblings.
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.
.. note::
Any values that have not been set from the registered defaults will have their default values
added to the dictionary that this method returns.
.. important:: .. important::
This method is overridden in :py:meth:`.MemberGroup.all_from_kind` and functions slightly differently. 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.
:rtype: dict
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
all_from_kind = await self._super_group() all_from_kind = await self._super_group()
@ -278,35 +330,47 @@ class Group(Value):
await super().set(value) await super().set(value)
async def set_attr(self, item: str, value): async def set_attr(self, item: str, value):
""" """Set an attribute by its name.
Please see :py:meth:`get_attr` for more information.
.. note:: Similar to `get_attr` in the way it can be used to dynamically set
attributes by name.
Note
----
Use of this method should be avoided wherever possible. Use of this method should be avoided wherever possible.
Parameters
----------
item : str
The name of the attribute being set.
value
The raw data value to set the attribute as.
""" """
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):
""" """Wipe all data from this group.
Wipes all data from the given Guild/Channel/Role/Member/User. If used on a global group, it will wipe all global
data. If used on a global group, it will wipe all global data, but not
local data.
""" """
await self.set({}) await self.set({})
async def clear_all(self): async def clear_all(self):
""" """Wipe all data from this group and its siblings.
Wipes all data from all Guilds/Channels/Roles/Members/Users. If used on a global group, this method wipes all
data from everything. If used on a global group, this method wipes all data from all groups.
""" """
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.
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`. Inherits from `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:
@ -329,29 +393,35 @@ class MemberGroup(Group):
return group_obj return group_obj
async def all_guilds(self) -> dict: async def all_guilds(self) -> dict:
""" """Get a dict of :code:`GUILD_ID -> MEMBER_ID -> data`.
Returns a dict of :code:`GUILD_ID -> MEMBER_ID -> data`.
.. note:: Note
----
The return value of this method will include registered defaults
for groups which have not had their values set.
Any values that have not been set from the registered defaults will have their default values Returns
added to the dictionary that this method returns. -------
dict
A dict of data from all members from all guilds.
:rtype: dict
""" """
# noinspection PyTypeChecker # noinspection PyTypeChecker
return await super().all_from_kind() return await super().all_from_kind()
async def all_from_kind(self) -> dict: async def all_from_kind(self) -> dict:
""" """Get a dict of all members from the same guild as the given one.
Returns a dict of all members from the same guild as the given one.
.. note:: Note
----
The return value of this method will include registered defaults
for groups which have not had their values set.
Any values that have not been set from the registered defaults will have their default values Returns
added to the dictionary that this method returns. -------
dict
A dict of :code:`MEMBER_ID -> data`.
:rtype: dict
""" """
guild_member = await super().all_from_kind() guild_member = await super().all_from_kind()
return guild_member.get(self.identifiers[-2], {}) return guild_member.get(self.identifiers[-2], {})
@ -359,8 +429,10 @@ class MemberGroup(Group):
class Config: class Config:
""" """Configuration manager for cogs and Red.
You should always use :func:`get_conf` or :func:`get_core_conf` to initialize a Config object.
You should always use `get_conf` or to instantiate a Config object. Use
`get_core_conf` for Config used in the core package.
.. important:: .. important::
Most config data should be accessed through its respective group method (e.g. :py:meth:`guild`) Most config data should be accessed through its respective group method (e.g. :py:meth:`guild`)
@ -369,27 +441,26 @@ class Config:
await conf.foo() await conf.foo()
.. py:attribute:: cog_name Attributes
----------
cog_name : `str`
The name of the cog that has requested a `Config` object.
unique_identifier : `int`
Unique identifier provided to differentiate cog data when name
conflicts occur.
spawner
A callable object that returns some driver that implements
`redbot.core.drivers.red_base.BaseDriver`.
force_registration : `bool`
Determines if Config should throw an error if a cog attempts to access
an attribute which has not been previously registered.
The name of the cog that has requested a :py:class:`.Config` object. Note
----
**You should use this.** By enabling force registration you give Config
the ability to alert you instantly if you've made a typo when
attempting to access data.
.. 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"
@ -416,19 +487,26 @@ class Config:
@classmethod @classmethod
def get_conf(cls, cog_instance, identifier: int, def get_conf(cls, cog_instance, identifier: int,
force_registration=False): force_registration=False):
""" """Get a Config instance for your cog.
Returns a Config instance based on a simplified set of initial
variables. Parameters
----------
cog_instance
This is an instance of your cog after it has been instantiated. If
you're calling this method from within your cog's :code:`__init__`,
this is just :code:`self`.
identifier : int
A (hard-coded) random integer, used to keep your data distinct from
any other cog with the same name.
force_registration : `bool`, optional
Should config require registration of data keys before allowing you
to get/set values? See `force_registration`.
Returns
-------
Config
A new Config object.
:param cog_instance:
: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
of data keys before allowing you to get/set values?
:return:
A new config object.
""" """
cog_path_override = cog_data_path(cog_instance) cog_path_override = cog_data_path(cog_instance)
cog_name = cog_path_override.stem cog_name = cog_path_override.stem
@ -441,15 +519,14 @@ 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
All core modules that require a config instance should use this classmethod instead of classmethod instead of `get_conf`.
:py:meth:`get_conf`
identifier : int
See `get_conf`.
force_registration : `bool`, optional
See `force_registration`.
:param int identifier:
See :py:meth:`get_conf`
:param force_registration:
See :py:attr:`force_registration`
:type force_registration: Optional[bool]
""" """
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,
@ -457,11 +534,23 @@ class Config:
force_registration=force_registration) force_registration=force_registration)
def __getattr__(self, item: str) -> Union[Group, Value]: def __getattr__(self, item: str) -> Union[Group, Value]:
""" """Same as `group.__getattr__` except for global data.
This is used to generate Value or Group objects for global
values. Parameters
:param item: ----------
:return: item : str
The attribute you want to get.
Returns
-------
`Group` or `Value`
The value for the attribute you want to retrieve
Raises
------
AttributeError
If there is no global attribute by the given name and
`force_registration` is set to :code:`True`.
""" """
global_group = self._get_base_group(self.GLOBAL) global_group = self._get_base_group(self.GLOBAL)
return getattr(global_group, item) return getattr(global_group, item)
@ -526,9 +615,11 @@ 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):
""" """Register default values for attributes you wish to store in `Config`
Registers default values for attributes you wish to store in :py:class:`.Config` at a global level. at a global level.
Examples
--------
You can register a single value or multiple values:: You can register a single value or multiple values::
conf.register_global( conf.register_global(
@ -562,37 +653,47 @@ class Config:
foo__bar=True, foo__bar=True,
foo__baz=False 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):
""" """Register default values on a per-guild level.
Registers 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)
def register_channel(self, **kwargs): def register_channel(self, **kwargs):
""" """Register default values on a per-channel level.
Registers default values on a per-channel level. See :py:meth:`register_global` for more details.
See `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-role level.
Registers default values on a per-role level. See :py:meth:`register_global` for more details.
See `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-user level.
Registers default values on a per-user level (i.e. server-independent). See :py:meth:`register_global` for more details.
This means that each user's data is guild-independent.
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 (i.e. server-dependent). See :py:meth:`register_global` for more details.
This means that each user's data is guild-dependent.
See `register_global` for more details.
""" """
self._register_default(self.MEMBER, **kwargs) self._register_default(self.MEMBER, **kwargs)
@ -607,43 +708,59 @@ class Config:
) )
def guild(self, guild: discord.Guild) -> Group: def guild(self, guild: discord.Guild) -> Group:
""" """Returns a `Group` for the given guild.
Returns a :py:class:`.Group` for the given guild.
Parameters
----------
guild : discord.Guild
A guild object.
: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 `Group` for the given channel.
Returns a :py:class:`.Group` for the given channel. This does not currently support differences between
text and voice channels. This does not discriminate between text and voice channels.
Parameters
----------
channel : `discord.abc.GuildChannel`
A channel object.
: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 `Group` for the given role.
Returns a :py:class:`.Group` for the given role.
Parameters
----------
role : discord.Role
A role object.
: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 `Group` for the given user.
Returns a :py:class:`.Group` for the given user.
Parameters
----------
user : discord.User
A user object.
: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 `Group` for the given member.
Returns a :py:class:`.MemberGroup` for the given member.
Parameters
----------
member : discord.Member
A member object.
: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,6 +1,7 @@
"""The purpose of this module is to allow for Red to """Module for Red's Context class
further customise the command invocation context provided
by discord.py. The purpose of this module is to allow for Red to further customise the command
invocation context provided by discord.py.
""" """
import discord import discord
@ -12,27 +13,37 @@ TICK = "\N{WHITE HEAVY CHECK MARK}"
class RedContext(commands.Context): class RedContext(commands.Context):
""" """Command invocation context for Red.
Command invocation context for Red.
All context passed into commands will be of this type. All context passed into commands will be of this type.
This class inherits from This class inherits from `commands.Context <discord.ext.commands.Context>`.
:py:class:`commands.Context <discord.ext.commands.Context>`.
""" """
async def send_help(self): async def send_help(self):
"""Send the command help message.""" """Send the command help message.
Returns
-------
`list` of `discord.Message`
A list of help messages which were sent to the user.
"""
command = self.invoked_subcommand or self.command command = self.invoked_subcommand or self.command
pages = await self.bot.formatter.format_help_for(self, command) pages = await self.bot.formatter.format_help_for(self, command)
ret = []
for page in pages: for page in pages:
await self.send(page) ret.append(await self.send(page))
return ret
async def tick(self): async def tick(self):
"""Add a tick reaction to the command message. """Add a tick reaction to the command message.
:return: ``True`` if adding the reaction succeeded. Returns
:rtype: bool -------
bool
:code:`True` if adding the reaction succeeded.
""" """
try: try:
await self.message.add_reaction(TICK) await self.message.add_reaction(TICK)

View File

@ -1,5 +1,6 @@
from typing import Tuple from typing import Tuple
__all__ = ["BaseDriver"]
class BaseDriver: class BaseDriver:
def get_driver(self): def get_driver(self):

View File

@ -5,6 +5,8 @@ from ..json_io import JsonIO
from .red_base import BaseDriver from .red_base import BaseDriver
__all__ = ["JSON"]
class JSON(BaseDriver): class JSON(BaseDriver):
def __init__(self, cog_name, *, data_path_override: Path=None, def __init__(self, cog_name, *, data_path_override: Path=None,

View File

@ -2,6 +2,10 @@ import pymongo as m
from .red_base import BaseDriver from .red_base import BaseDriver
__all__ = ["Mongo", "RedMongoException", "MultipleMatches",
"MissingCollection"]
class RedMongoException(Exception): class RedMongoException(Exception):
"""Base Red Mongo Exception class""" """Base Red Mongo Exception class"""
pass pass