diff --git a/docs/audio.rst b/docs/audio.rst index f05f8cf1..1b163fe9 100644 --- a/docs/audio.rst +++ b/docs/audio.rst @@ -7,3 +7,6 @@ royalnet.audio .. automodule:: royalnet.audio :members: + :private-members: + :undoc-members: + diff --git a/docs/bots.rst b/docs/bots.rst index a3392a7a..42f1173b 100644 --- a/docs/bots.rst +++ b/docs/bots.rst @@ -4,5 +4,8 @@ royalnet.bots .. toctree:: :maxdepth: 2 + .. automodule:: royalnet.bots :members: + :private-members: + :undoc-members: diff --git a/docs/commands.rst b/docs/commands.rst index 62043493..9f8befe2 100644 --- a/docs/commands.rst +++ b/docs/commands.rst @@ -7,3 +7,5 @@ royalnet.commands .. automodule:: royalnet.commands :members: + :private-members: + :special-members: diff --git a/docs/conf.py b/docs/conf.py index 02352318..ce54d06b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -32,6 +32,17 @@ extensions = ["sphinx.ext.autodoc", "sphinx.ext.napoleon", "sphinx.ext.intersphi intersphinx_mapping = {'python': ('https://docs.python.org/3.7', None), 'discord': ('https://discordpy.readthedocs.io/en/latest/', None)} + +def skip(app, what, name, obj, would_skip, options): + if name == "__init__": + return False + return would_skip + + +def setup(app): + app.connect("autodoc-skip-member", skip) + + # 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 478f8c1c..64293c67 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -7,6 +7,8 @@ royalnet.database .. automodule:: royalnet.database :members: + :private-members: + :undoc-members: Tables @@ -14,3 +16,5 @@ Tables .. automodule:: royalnet.database.tables :members: + :private-members: + :undoc-members: diff --git a/docs/network.rst b/docs/network.rst index cc5b52f4..5c07dbbe 100644 --- a/docs/network.rst +++ b/docs/network.rst @@ -7,3 +7,6 @@ royalnet.network .. automodule:: royalnet.network :members: + :private-members: + :undoc-members: + diff --git a/docs/utils.rst b/docs/utils.rst index 9d098917..1deb9cbc 100644 --- a/docs/utils.rst +++ b/docs/utils.rst @@ -7,3 +7,6 @@ royalnet.utils .. automodule:: royalnet.utils :members: + :private-members: + :undoc-members: + diff --git a/royalnet/audio/playmodes.py b/royalnet/audio/playmodes.py index a7ffe40f..7b8d59c3 100644 --- a/royalnet/audio/playmodes.py +++ b/royalnet/audio/playmodes.py @@ -10,36 +10,52 @@ class 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.generate_generator() + self.generator: typing.AsyncGenerator = self._generate_generator() - async def next(self): + async def next(self) -> typing.Optional[RoyalPCMAudio]: + """Get the next :py:class:`royalnet.audio.RoyalPCMAudio` from the list and advance it. + + Returns: + The next :py:class:`royalnet.audio.RoyalPCMAudio`.""" return await self.generator.__anext__() 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.""" + Returns: + Usually a :py:class:`int`, but may return also :py:obj:`math.inf` if the PlayMode is infinite.""" raise NotImplementedError() - async def generate_generator(self): - """Get the next :py:class:`royalnet.audio.RoyalPCMAudio` from the list and advance it.""" + async def _generate_generator(self): + """Factory function for an async generator that changes the ``now_playing`` property either to a :py:class:`discord.audio.RoyalPCMAudio` or to ``None``, then yields the value it changed it to. + + Yields: + The :py:class:`royalnet.audio.RoyalPCMAudio` to be played next.""" raise NotImplementedError() # This is needed to make the coroutine an async generator # noinspection PyUnreachableCode yield NotImplemented - def add(self, item): - """Add a new :py:class:`royalnet.audio.RoyalPCMAudio` to the PlayMode.""" + def add(self, item: RoyalPCMAudio) -> None: + """Add a new :py:class:`royalnet.audio.RoyalPCMAudio` to the PlayMode. + + Args: + item: The item to add to the PlayMode.""" raise NotImplementedError() - def delete(self): + def delete(self) -> None: """Delete all :py:class:`royalnet.audio.RoyalPCMAudio` contained inside this PlayMode.""" raise NotImplementedError() class Playlist(PlayMode): """A video list. :py:class:`royalnet.audio.RoyalPCMAudio` played are removed from the list.""" + def __init__(self, starting_list: typing.List[RoyalPCMAudio] = None): + """Create a new Playlist. + + Args: + starting_list: A list of items with which the Playlist will be created.""" super().__init__() if starting_list is None: starting_list = [] @@ -48,7 +64,7 @@ class Playlist(PlayMode): def videos_left(self) -> typing.Union[int, float]: return len(self.list) - async def generate_generator(self): + async def _generate_generator(self): while True: try: next_video = self.list.pop(0) @@ -60,10 +76,10 @@ class Playlist(PlayMode): if self.now_playing is not None: self.now_playing.delete() - def add(self, item): + def add(self, item) -> None: self.list.append(item) - def delete(self): + def delete(self) -> None: while self.list: self.list.pop(0).delete() self.now_playing.delete() @@ -71,7 +87,12 @@ class Playlist(PlayMode): class Pool(PlayMode): """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): + """Create a new Pool. + + Args: + starting_pool: A list of items the Pool will be created from.""" super().__init__() if starting_pool is None: starting_pool = [] @@ -81,7 +102,7 @@ class Pool(PlayMode): def videos_left(self) -> typing.Union[int, float]: return math.inf - async def generate_generator(self): + async def _generate_generator(self): while True: if not self.pool: self.now_playing = None @@ -94,12 +115,12 @@ class Pool(PlayMode): self.now_playing = next_video yield next_video - def add(self, item): + def add(self, item) -> None: self.pool.append(item) self._pool_copy.append(item) random.shuffle(self._pool_copy) - def delete(self): + def delete(self) -> None: for item in self.pool: item.delete() self.pool = None diff --git a/royalnet/audio/royalpcmaudio.py b/royalnet/audio/royalpcmaudio.py index ca46c50d..3a4ed425 100644 --- a/royalnet/audio/royalpcmaudio.py +++ b/royalnet/audio/royalpcmaudio.py @@ -8,26 +8,35 @@ 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 """ + """Create a :py:class:`discord.audio.RoyalPCMAudio` from a :py:class:`royalnet.audio.RoyalPCMFile`. + + Warning: + Not recommended, use :py:func:`royalnet.audio.RoyalPCMAudio.create_from_url` or :py:func:`royalnet.audio.RoyalPCMAudio.create_from_ytsearch` instead.""" 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`. + """Download a file with youtube_dl and create a list of RoyalPCMAudios. Parameters: - url: The url of the file to download.""" + url: The url of the file to download. + + Returns: + A :py:class:`list` of RoyalPCMAudios, each corresponding to a downloaded video.""" 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`. + """Search a string on YouTube and download the first ``amount`` number of videos, then download those with youtube_dl and create a list of RoyalPCMAudios. Parameters: search: The string to search on YouTube. - amount: The number of videos to download.""" + amount: The number of videos to download. + + Returns: + A :py:class:`list` of RoyalPCMAudios, each corresponding to a downloaded video.""" rpf_list = RoyalPCMFile.create_from_ytsearch(search, amount) return [RoyalPCMAudio(rpf) for rpf in rpf_list] diff --git a/royalnet/audio/royalpcmfile.py b/royalnet/audio/royalpcmfile.py index 84d1795e..6be1544e 100644 --- a/royalnet/audio/royalpcmfile.py +++ b/royalnet/audio/royalpcmfile.py @@ -21,12 +21,12 @@ class RoyalPCMFile(YtdlFile): # Set the time to generate the filename self._time = time.time() # Ensure the file doesn't already exist - if os.path.exists(self._ytdl_filename) or os.path.exists(self.audio_filename): + if os.path.exists(self.ytdl_filename) or os.path.exists(self.audio_filename): raise FileExistsError("Can't overwrite file") # Overwrite the new ytdl_args self.ytdl_args = {**self.ytdl_args, **ytdl_args} log.info(f"Now downloading {info.webpage_url}") - super().__init__(info, outtmpl=self._ytdl_filename, **self.ytdl_args) + super().__init__(info, outtmpl=self.ytdl_filename, **self.ytdl_args) # Find the audio_filename with a regex (should be video.opus) log.info(f"Converting {self.video_filename}...") # Convert the video to pcm @@ -50,25 +50,48 @@ class RoyalPCMFile(YtdlFile): """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.""" + url: The url of the file to download. + ytdl_args: Extra arguments to be passed to YoutubeDL while downloading. + + Returns: + A :py:class:`list` of RoyalPCMAudios, each corresponding to a downloaded video.""" 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`` number of videos, then download those with youtube_dl and create a list of :py:class:`discord.audio.RoyalPCMFile`.""" + """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`. + + Parameters: + search: The string to search on YouTube. + amount: The number of videos to download. + ytdl_args: Extra arguments to be passed to YoutubeDL while downloading. + + Returns: + A :py:class:`list` of RoyalPCMFiles, each corresponding to a downloaded video.""" url = f"ytsearch{amount}:{search}" info_list = YtdlInfo.create_from_url(url) return [RoyalPCMFile(info, **ytdl_args) for info in info_list] @property - def _ytdl_filename(self): + def ytdl_filename(self) -> str: + """ + Returns: + The name of the downloaded video file, as a :py:class:`str`. + + Warning: + It's going to be deleted as soon as the :py:func:`discord.audio.RoyalPCMFile.__init__` function has completed, so it's probably not going to be very useful... + """ return f"./downloads/{safefilename(self.info.title)}-{safefilename(str(int(self._time)))}.ytdl" @property - def audio_filename(self): + def audio_filename(self) -> str: + """ + Returns: + The name of the downloaded and PCM-converted audio file.""" return f"./downloads/{safefilename(self.info.title)}-{safefilename(str(int(self._time)))}.pcm" def delete_audio_file(self): + """Delete the PCM-converted audio file.""" log.info(f"Deleting {self.audio_filename}") os.remove(self.audio_filename) diff --git a/royalnet/audio/youtubedl.py b/royalnet/audio/youtubedl.py index 723dbe6d..f43f232b 100644 --- a/royalnet/audio/youtubedl.py +++ b/royalnet/audio/youtubedl.py @@ -15,6 +15,8 @@ class InterruptDownload(DownloaderError): class YtdlFile: + """A wrapper around a youtube_dl downloaded file.""" + ytdl_args = { "logger": log, # Log messages to a logging.Logger instance. "quiet": True, # Do not print messages to stdout. @@ -22,7 +24,6 @@ class YtdlFile: "no_warnings": True, # Do not print out anything for warnings. } - """A wrapper around a youtube_dl downloaded file.""" def __init__(self, info: "YtdlInfo", outtmpl="%(title)s-%(id)s.%(ext)s", **ytdl_args): self.info: "YtdlInfo" = info self.video_filename: str @@ -57,7 +58,11 @@ class YtdlFile: class YtdlInfo: """A wrapper around youtube_dl extracted info.""" - def __init__(self, info): + def __init__(self, info: typing.Dict[str, typing.Any]): + """Create a YtdlInfo from the dict returned by the :py:func:`youtube_dl.YoutubeDL.extract_info` function. + + Warning: + Does not download the info, for that use :py:func:`royalnet.audio.YtdlInfo.create_from_url`.""" self.id: typing.Optional[str] = info.get("id") self.uploader: typing.Optional[str] = info.get("uploader") self.uploader_id: typing.Optional[str] = info.get("uploader_id") diff --git a/royalnet/network/messages.py b/royalnet/network/messages.py index 81dc85e5..203316d2 100644 --- a/royalnet/network/messages.py +++ b/royalnet/network/messages.py @@ -2,42 +2,70 @@ from ..error import RoyalnetError class Message: + """A message sent through the Royalnet.""" def __repr__(self): return f"<{self.__class__.__name__}>" - def raise_on_error(self): - pass - class IdentifySuccessfulMessage(Message): - pass + """The Royalnet identification step was successful.""" class ServerErrorMessage(Message): + """Something went wrong in the connection to the :py:class:`royalnet.network.RoyalnetServer`.""" def __init__(self, reason): super().__init__() self.reason = reason class InvalidSecretEM(ServerErrorMessage): - pass + """The sent secret was incorrect. + + This message terminates connection to the :py:class:`royalnet.network.RoyalnetServer`.""" class InvalidPackageEM(ServerErrorMessage): - pass + """The sent :py:class:`royalnet.network.Package` was invalid.""" class InvalidDestinationEM(InvalidPackageEM): - pass + """The :py:class:`royalnet.network.Package` destination was invalid or not found.""" -class RequestSuccessful(Message): - pass +class Reply(Message): + """A reply to a request sent through the Royalnet.""" + + def raise_on_error(self) -> None: + """If the reply is an error, raise an error, otherwise, do nothing. + + Raises: + A :py:exc:`RoyalnetError`, if the Reply is an error, otherwise, nothing.""" + raise NotImplementedError() -class RequestError(Message): +class RequestSuccessful(Reply): + """The sent request was successful.""" + + def raise_on_error(self) -> None: + """If the reply is an error, raise an error, otherwise, do nothing. + + Does nothing.""" + pass + + +class RequestError(Reply): + """The sent request wasn't successful.""" + def __init__(self, exc: Exception): + """Create a RequestError. + + Parameters: + exc: The exception that caused the error in the request.""" self.exc: Exception = exc - def raise_on_error(self): + def raise_on_error(self) -> None: + """If the reply is an error, raise an error, otherwise, do nothing. + + Raises: + Always raises a :py:exc:`royalnet.error.RoyalnetError`, containing the exception that caused the error.""" raise RoyalnetError(exc=self.exc) diff --git a/royalnet/network/packages.py b/royalnet/network/packages.py index dde7d572..3cde55db 100644 --- a/royalnet/network/packages.py +++ b/royalnet/network/packages.py @@ -3,20 +3,42 @@ import uuid class Package: + """A Royalnet package, the data type with which a :py:class:`royalnet.network.RoyalnetLink` communicates with a :py:class:`royalnet.network.RoyalnetServer` or another link. """ + def __init__(self, data, destination: str, source: str, *, source_conv_id: str = None, destination_conv_id: str = None): + """Create a Package. + + Parameters: + data: The data that should be sent. Usually a :py:class:`royalnet.network.Message`. + destination: The ``link_type`` of the destination node, or alternatively, the ``nid`` of the node. Can also be the ``NULL`` value to send the message to nobody. + source: The ``nid`` of the node that created this Package. + source_conv_id: The conversation id of the node that created this package. Akin to the sequence number on IP packets. + destination_conv_id: The conversation id of the node that this Package is a reply to.""" + # TODO: something is not right in these type hints. Check them. self.data = data self.destination: str = destination - self.source = source - self.source_conv_id = source_conv_id or str(uuid.uuid4()) - self.destination_conv_id = destination_conv_id + self.source: str = source + self.source_conv_id: str = source_conv_id or str(uuid.uuid4()) + self.destination_conv_id: str = destination_conv_id def __repr__(self): return f"" def reply(self, data) -> "Package": + """Reply to this Package with another Package. + + Parameters: + data: The data that should be sent. Usually a :py:class:`royalnet.network.Message`. + + Returns: + The reply Package.""" return Package(data, self.source, self.destination, source_conv_id=str(uuid.uuid4()), destination_conv_id=self.source_conv_id) - def pickle(self): + def pickle(self) -> bytes: + """:py:mod:`pickle` this Package. + + Returns: + The pickled package in form of bytes.""" return pickle.dumps(self) diff --git a/royalnet/network/royalnetserver.py b/royalnet/network/royalnetserver.py index d63f92e0..bdf3fde4 100644 --- a/royalnet/network/royalnetserver.py +++ b/royalnet/network/royalnetserver.py @@ -85,7 +85,13 @@ class RoyalnetServer: self._loop.create_task(self.route_package(package)) def find_destination(self, package: Package) -> typing.List[ConnectedClient]: - """Find a list of destinations for the sent packages""" + """Find a list of destinations for the package. + + Parameters: + package: The package to find the destination of. + + Returns: + A :py:class:`list` of :py:class:`ConnectedClients` to send the package to.""" # Parse destination # Is it nothing? if package.destination == "NULL":