diff --git a/docs/conf.py b/docs/conf.py index 81205bd6..02352318 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,10 @@ author = 'Stefano Pigozzi' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ["sphinx.ext.autodoc"] +extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.intersphinx"] + +intersphinx_mapping = {'python': ('https://docs.python.org/3.7', None), + 'discord': ('https://discordpy.readthedocs.io/en/latest/', None)} # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/database.rst b/docs/database.rst index 23dd02a4..478f8c1c 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -7,3 +7,10 @@ royalnet.database .. automodule:: royalnet.database :members: + + +Tables +------------------------------------ + +.. automodule:: royalnet.database.tables + :members: diff --git a/royalnet/audio/playmodes.py b/royalnet/audio/playmodes.py index 93695a5c..a7ffe40f 100644 --- a/royalnet/audio/playmodes.py +++ b/royalnet/audio/playmodes.py @@ -5,43 +5,50 @@ from .royalpcmaudio import RoyalPCMAudio class PlayMode: + """The base class for a PlayMode, such as :py:class:`royalnet.audio.Playlist`. Inherit from this class if you want to create a custom PlayMode.""" + def __init__(self): + """Create a new PlayMode and initialize the generator inside.""" self.now_playing: typing.Optional[RoyalPCMAudio] = None - self.generator: typing.AsyncGenerator = self._generator() + self.generator: typing.AsyncGenerator = self.generate_generator() async def next(self): return await self.generator.__anext__() - def videos_left(self): + def videos_left(self) -> typing.Union[int, float]: + """Return the number of videos left in the PlayMode. + + Usually returns an :py:class:`int`, but may return :py:obj:`math.inf` if the PlayMode is infinite.""" raise NotImplementedError() - async def _generator(self): - """Get the next RPA from the list and advance it.""" + async def generate_generator(self): + """Get the next :py:class:`royalnet.audio.RoyalPCMAudio` from the list and advance it.""" raise NotImplementedError() # This is needed to make the coroutine an async generator # noinspection PyUnreachableCode yield NotImplemented def add(self, item): - """Add a new RPA to the PlayMode.""" + """Add a new :py:class:`royalnet.audio.RoyalPCMAudio` to the PlayMode.""" raise NotImplementedError() def delete(self): - """Delete all RPAs contained inside this PlayMode.""" + """Delete all :py:class:`royalnet.audio.RoyalPCMAudio` contained inside this PlayMode.""" + raise NotImplementedError() class Playlist(PlayMode): - """A video list. RPAs played are removed from the list.""" + """A video list. :py:class:`royalnet.audio.RoyalPCMAudio` played are removed from the list.""" def __init__(self, starting_list: typing.List[RoyalPCMAudio] = None): super().__init__() if starting_list is None: starting_list = [] self.list: typing.List[RoyalPCMAudio] = starting_list - def videos_left(self): + def videos_left(self) -> typing.Union[int, float]: return len(self.list) - async def _generator(self): + async def generate_generator(self): while True: try: next_video = self.list.pop(0) @@ -63,7 +70,7 @@ class Playlist(PlayMode): class Pool(PlayMode): - """A RPA pool. RPAs played are played back in random order, and they are kept in the pool.""" + """A :py:class:`royalnet.audio.RoyalPCMAudio` pool. :py:class:`royalnet.audio.RoyalPCMAudio` are selected in random order and are not repeated until every song has been played at least once.""" def __init__(self, starting_pool: typing.List[RoyalPCMAudio] = None): super().__init__() if starting_pool is None: @@ -71,10 +78,10 @@ class Pool(PlayMode): self.pool: typing.List[RoyalPCMAudio] = starting_pool self._pool_copy: typing.List[RoyalPCMAudio] = [] - def videos_left(self): + def videos_left(self) -> typing.Union[int, float]: return math.inf - async def _generator(self): + async def generate_generator(self): while True: if not self.pool: self.now_playing = None diff --git a/royalnet/audio/royalpcmaudio.py b/royalnet/audio/royalpcmaudio.py index ae883095..ca46c50d 100644 --- a/royalnet/audio/royalpcmaudio.py +++ b/royalnet/audio/royalpcmaudio.py @@ -5,17 +5,29 @@ from .royalpcmfile import RoyalPCMFile class RoyalPCMAudio(AudioSource): + """A discord-compatible :py:class:`discord.AudioSource` that keeps data in a file instead of in memory.""" + def __init__(self, rpf: "RoyalPCMFile"): + """Create a :py:class:`discord.audio.RoyalPCMAudio` from a :ref:`discord.audio.RoyalPCMFile`. Not recommended, use """ self.rpf: "RoyalPCMFile" = rpf self._file = open(self.rpf.audio_filename, "rb") @staticmethod def create_from_url(url: str) -> typing.List["RoyalPCMAudio"]: + """Download a file with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMAudio`. + + Parameters: + url: The url of the file to download.""" rpf_list = RoyalPCMFile.create_from_url(url) return [RoyalPCMAudio(rpf) for rpf in rpf_list] @staticmethod def create_from_ytsearch(search: str, amount: int = 1) -> typing.List["RoyalPCMAudio"]: + """Search a string on YouTube and download the first ``amount`` number of videos, then download those with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMAudio`. + + Parameters: + search: The string to search on YouTube. + amount: The number of videos to download.""" rpf_list = RoyalPCMFile.create_from_ytsearch(search, amount) return [RoyalPCMAudio(rpf) for rpf in rpf_list] @@ -36,6 +48,7 @@ class RoyalPCMAudio(AudioSource): return data def delete(self): + """Permanently delete the downloaded file.""" self._file.close() self.rpf.delete_audio_file() diff --git a/royalnet/audio/royalpcmfile.py b/royalnet/audio/royalpcmfile.py index 3c87998f..84d1795e 100644 --- a/royalnet/audio/royalpcmfile.py +++ b/royalnet/audio/royalpcmfile.py @@ -46,13 +46,17 @@ class RoyalPCMFile(YtdlFile): return f"" @staticmethod - def create_from_url(url, **ytdl_args) -> typing.List["RoyalPCMFile"]: + def create_from_url(url: str, **ytdl_args) -> typing.List["RoyalPCMFile"]: + """Download a file with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMFile`. + + Parameters: + url: The url of the file to download.""" info_list = YtdlInfo.create_from_url(url) return [RoyalPCMFile(info, **ytdl_args) for info in info_list] @staticmethod def create_from_ytsearch(search: str, amount: int = 1, **ytdl_args) -> typing.List["RoyalPCMFile"]: - """Search a string on YouTube and download the first amount videos found.""" + """Search a string on YouTube and download the first ``amount`` number of videos, then download those with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMFile`.""" url = f"ytsearch{amount}:{search}" info_list = YtdlInfo.create_from_url(url) return [RoyalPCMFile(info, **ytdl_args) for info in info_list] diff --git a/royalnet/bots/__init__.py b/royalnet/bots/__init__.py index 59c8fbc1..041be790 100644 --- a/royalnet/bots/__init__.py +++ b/royalnet/bots/__init__.py @@ -1,4 +1,5 @@ from .telegram import TelegramBot, TelegramConfig from .discord import DiscordBot, DiscordConfig +from .generic import GenericBot -__all__ = ["TelegramBot", "TelegramConfig", "DiscordBot", "DiscordConfig"] +__all__ = ["TelegramBot", "TelegramConfig", "DiscordBot", "DiscordConfig", "GenericBot"] diff --git a/royalnet/bots/discord.py b/royalnet/bots/discord.py index ab2c1834..6fe796b1 100644 --- a/royalnet/bots/discord.py +++ b/royalnet/bots/discord.py @@ -19,11 +19,14 @@ if not discord.opus.is_loaded(): class DiscordConfig: + """The specific configuration to be used for :ref:`royalnet.database.DiscordBot`.""" def __init__(self, token: str): self.token = token class DiscordBot(GenericBot): + """A bot that connects to `Discord `_.""" + interface_name = "discord" def _init_voice(self): diff --git a/royalnet/bots/generic.py b/royalnet/bots/generic.py index 41d82f8f..305ec234 100644 --- a/royalnet/bots/generic.py +++ b/royalnet/bots/generic.py @@ -12,7 +12,7 @@ log = logging.getLogger(__name__) class GenericBot: - """A generic bot class, to be used as base for the other more specific classes, such as TelegramBot and DiscordBot.""" + """A generic bot class, to be used as base for the other more specific classes, such as :ref:`royalnet.bots.TelegramBot` and :ref:`royalnet.bots.DiscordBot`.""" interface_name = NotImplemented def _init_commands(self, diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py index 3bea21c0..c30a75be 100644 --- a/royalnet/bots/telegram.py +++ b/royalnet/bots/telegram.py @@ -13,16 +13,14 @@ loop = asyncio.get_event_loop() log = _logging.getLogger(__name__) -async def todo(message: Message): - log.warning(f"Skipped {message} because handling isn't supported yet.") - - class TelegramConfig: + """The specific configuration to be used for :ref:`royalnet.database.TelegramBot`.""" def __init__(self, token: str): self.token: str = token class TelegramBot(GenericBot): + """A bot that connects to `Telegram `_.""" interface_name = "telegram" def _init_client(self): diff --git a/royalnet/database/alchemy.py b/royalnet/database/alchemy.py index e17d4041..c413f343 100644 --- a/royalnet/database/alchemy.py +++ b/royalnet/database/alchemy.py @@ -12,7 +12,15 @@ loop = asyncio.get_event_loop() class Alchemy: - def __init__(self, database_uri: str, tables: typing.Optional[typing.Set] = None): + """A wrapper around SQLAlchemy declarative that allows to use multiple databases at once while maintaining a single table-class for both of them.""" + + def __init__(self, database_uri: str, tables: typing.Set): + """Create a new Alchemy object. + + Args: + database_uri: The uri of the database, as described at https://docs.sqlalchemy.org/en/13/core/engines.html . + tables: The set of tables to be created and used in the selected database. Check the tables submodule for more details. + """ if database_uri.startswith("sqlite"): raise NotImplementedError("Support for sqlite databases is currently missing") self.engine = create_engine(database_uri) @@ -20,7 +28,7 @@ class Alchemy: self.Session = sessionmaker(bind=self.engine) self._create_tables(tables) - def _create_tables(self, tables: typing.Optional[typing.List]): + def _create_tables(self, tables: typing.Set): for table in tables: name = table.__name__ try: @@ -34,7 +42,8 @@ class Alchemy: self.Base.metadata.create_all() @contextmanager - async def session_cm(self): + def session_cm(self): + """Use Alchemy as a context manager (to be used in with statements).""" session = self.Session() try: yield session @@ -46,6 +55,7 @@ class Alchemy: @asynccontextmanager async def session_acm(self): + """Use Alchemy as a asyncronous context manager (to be used in async with statements).""" session = await asyncify(self.Session) try: yield session diff --git a/royalnet/database/databaseconfig.py b/royalnet/database/databaseconfig.py index c0f12165..3cfae6c4 100644 --- a/royalnet/database/databaseconfig.py +++ b/royalnet/database/databaseconfig.py @@ -2,6 +2,8 @@ import typing class DatabaseConfig: + """The configuration to be used for the :ref:`royalnet.database.Alchemy` component of :ref:`royalnet.bots.GenericBot`.""" + def __init__(self, database_uri: str, master_table: typing.Type,