From 6450d99a0f079afbed2adc9967249fd31ac0dd37 Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sun, 6 May 2012 03:11:16 -0400 Subject: [PATCH] Better docstrings for a bunch of modules --- docs/api/earwigbot.rst | 6 +--- docs/customizing.rst | 2 +- earwigbot/__init__.py | 9 ++--- earwigbot/bot.py | 31 +++++++++-------- earwigbot/config.py | 90 ++++++++++++++++++++++++++----------------------- earwigbot/exceptions.py | 8 +++++ earwigbot/managers.py | 39 +++++++++++---------- earwigbot/util.py | 35 ++++++++++++++++--- earwigbot/wiki/page.py | 5 ++- earwigbot/wiki/site.py | 9 +++-- 10 files changed, 135 insertions(+), 99 deletions(-) diff --git a/docs/api/earwigbot.rst b/docs/api/earwigbot.rst index 07c8499..145f022 100644 --- a/docs/api/earwigbot.rst +++ b/docs/api/earwigbot.rst @@ -7,7 +7,6 @@ earwigbot Package .. automodule:: earwigbot.__init__ :members: :undoc-members: - :show-inheritance: :mod:`bot` Module ----------------- @@ -15,7 +14,6 @@ earwigbot Package .. automodule:: earwigbot.bot :members: :undoc-members: - :show-inheritance: :mod:`config` Module -------------------- @@ -23,7 +21,6 @@ earwigbot Package .. automodule:: earwigbot.config :members: :undoc-members: - :show-inheritance: :mod:`exceptions` Module ------------------------ @@ -37,7 +34,7 @@ earwigbot Package ---------------------- .. automodule:: earwigbot.managers - :members: + :members: _ResourceManager, CommandManager, TaskManager :undoc-members: :show-inheritance: @@ -47,7 +44,6 @@ earwigbot Package .. automodule:: earwigbot.util :members: :undoc-members: - :show-inheritance: Subpackages ----------- diff --git a/docs/customizing.rst b/docs/customizing.rst index dde9b67..fa4be59 100644 --- a/docs/customizing.rst +++ b/docs/customizing.rst @@ -57,7 +57,7 @@ The most useful attributes are: logged and used as the quit message when disconnecting from IRC. :py:class:`earwigbot.config.BotConfig` stores configuration information for the -bot. Its docstring explains what each attribute is used for, but essentially +bot. Its docstrings explains what each attribute is used for, but essentially each "node" (one of :py:attr:`config.components`, :py:attr:`wiki`, :py:attr:`tasks`, :py:attr:`tasks`, or :py:attr:`metadata`) maps to a section of the bot's :file:`config.yml` file. For example, if :file:`config.yml` diff --git a/earwigbot/__init__.py b/earwigbot/__init__.py index 236104c..a9e7ed3 100644 --- a/earwigbot/__init__.py +++ b/earwigbot/__init__.py @@ -21,11 +21,12 @@ # SOFTWARE. """ -EarwigBot is a Python robot that edits Wikipedia and interacts with people over -IRC. - https://github.com/earwig/earwigbot +`EarwigBot `_ is a Python robot that edits +Wikipedia and interacts with people over IRC. -See README.rst for an overview, or the docs/ directory for details. This -documentation is also available online at http://packages.python.org/earwigbot. +See :file:`README.rst` for an overview, or the :file:`docs/` directory for +details. This documentation is also available `online +`_. """ __author__ = "Ben Kurtovic" diff --git a/earwigbot/bot.py b/earwigbot/bot.py index 30bcfeb..c80d4fc 100644 --- a/earwigbot/bot.py +++ b/earwigbot/bot.py @@ -40,18 +40,21 @@ class Bot(object): EarwigBot has three components that can run independently of each other: an IRC front-end, an IRC watcher, and a wiki scheduler. - * The IRC front-end runs on a normal IRC server and expects users to + - The IRC front-end runs on a normal IRC server and expects users to interact with it/give it commands. - * The IRC watcher runs on a wiki recent-changes server and listens for + - The IRC watcher runs on a wiki recent-changes server and listens for edits. Users cannot interact with this part of the bot. - * The wiki scheduler runs wiki-editing bot tasks in separate threads at + - The wiki scheduler runs wiki-editing bot tasks in separate threads at user-defined times through a cron-like interface. - - The Bot() object is accessable from within commands and tasks as self.bot. - This is the primary way to access data from other components of the bot. - For example, our BotConfig object is accessable from bot.config, tasks - can be started with bot.tasks.start(), and sites can be loaded from the - wiki toolset with bot.wiki.get_site(). + + The :py:class:`Bot` object is accessable from within commands and tasks as + :py:attr:`self.bot`. This is the primary way to access data from other + components of the bot. For example, our + :py:class:`~earwigbot.config.BotConfig` object is accessable from + :py:attr:`bot.config`, tasks can be started with + :py:meth:`bot.tasks.start `, and + sites can be loaded from the wiki toolset with :py:meth:`bot.wiki.get_site + `. """ def __init__(self, root_dir, level=logging.INFO): @@ -160,11 +163,11 @@ class Bot(object): This is thread-safe, and it will gracefully stop IRC components before reloading anything. Note that you can safely reload commands or tasks - without restarting the bot with bot.commands.load() or - bot.tasks.load(). These should not interfere with running components - or tasks. + without restarting the bot with :py:meth:`bot.commands.load` or + :py:meth:`bot.tasks.load`. These should not interfere with running + components or tasks. - If given, 'msg' will be used as our quit message. + If given, *msg* will be used as our quit message. """ if msg: self.logger.info('Restarting bot ("{0}")'.format(msg)) @@ -180,7 +183,7 @@ class Bot(object): def stop(self, msg=None): """Gracefully stop all bot components. - If given, 'msg' will be used as our quit message. + If given, *msg* will be used as our quit message. """ if msg: self.logger.info('Stopping bot ("{0}")'.format(msg)) diff --git a/earwigbot/config.py b/earwigbot/config.py index 8b7b20a..8d2c980 100644 --- a/earwigbot/config.py +++ b/earwigbot/config.py @@ -29,36 +29,34 @@ from os import mkdir, path from Crypto.Cipher import Blowfish import yaml +from earwigbot.exceptions import NoConfigError + __all__ = ["BotConfig"] class BotConfig(object): """ - EarwigBot's YAML Config File Manager + **EarwigBot's YAML Config File Manager** This handles all tasks involving reading and writing to our config file, including encrypting and decrypting passwords and making a new config file from scratch at the inital bot run. - BotConfig has a few properties and functions, including the following: - - * config.root_dir - bot's working directory; contains config.yml, logs/ - * config.path - path to the bot's config file - * config.components - enabled components - * config.wiki - information about wiki-editing - * config.tasks - information for bot tasks - * config.irc - information about IRC - * config.metadata - miscellaneous information - * config.schedule() - tasks scheduled to run at a given time - - BotConfig also has some functions used in config loading: - - * config.load() - loads and parses our config file, returning True if - passwords are stored encrypted or False otherwise; - can also be used to easily reload config - * config.decrypt() - given a key, decrypts passwords inside our config - variables, and remembers to decrypt the password if - config is reloaded; won't do anything if passwords - aren't encrypted + BotConfig has a few attributes and methods, including the following: + + - :py:attr:`root_dir`: bot's working directory; contains + :file:`config.yml`, :file:`logs/` + - :py:attr:`path`: path to the bot's config file + - :py:attr:`components`: enabled components + - :py:attr:`wiki`: information about wiki-editing + - :py:attr:`tasks`: information for bot tasks + - :py:attr:`irc`: information about IRC + - :py:attr:`metadata`: miscellaneous information + - :py:meth:`schedule`: tasks scheduled to run at a given time + + BotConfig also has some methods used in config loading: + + - :py:meth:`load`: loads (or reloads) and parses our config file + - :py:meth:`decrypt`: decrypts an object in the config tree """ def __init__(self, root_dir, level): @@ -159,10 +157,12 @@ class BotConfig(object): @property def root_dir(self): + """The bot's root directory containing its config file and more.""" return self._root_dir @property def logging_level(self): + """The minimum logging level for messages logged via stdout.""" return self._logging_level @logging_level.setter @@ -172,15 +172,17 @@ class BotConfig(object): @property def path(self): + """The path to the bot's config file.""" return self._config_path @property def log_dir(self): + """The directory containing the bot's logs.""" return self._log_dir @property def data(self): - """The entire config file.""" + """The entire config file as a decoded JSON object.""" return self._data @property @@ -209,11 +211,11 @@ class BotConfig(object): return self._metadata def is_loaded(self): - """Return True if our config file has been loaded, otherwise False.""" + """Return ``True`` if our config file has been loaded, or ``False``.""" return self._data is not None def is_encrypted(self): - """Return True if passwords are encrypted, otherwise False.""" + """Return ``True`` if passwords are encrypted, otherwise ``False``.""" return self.metadata.get("encryptPasswords", False) def load(self): @@ -223,12 +225,14 @@ class BotConfig(object): user. If there is no config file at all, offer to make one, otherwise exit. - Store data from our config file in five _ConfigNodes (components, - wiki, tasks, irc, metadata) for easy access (as well as the internal - _data variable). - - If config is being reloaded, encrypted items will be automatically - decrypted if they were decrypted beforehand. + Data from the config file is stored in five + :py:class:`~earwigbot.config._ConfigNode` s (:py:attr:`components`, + :py:attr:`wiki`, :py:attr:`tasks`, :py:attr:`irc`, :py:attr:`metadata`) + for easy access (as well as the lower-level :py:attr:`data` attribute). + If passwords are encrypted, we'll use :py:func:`~getpass.getpass` for + the key and then decrypt them. If the config is being reloaded, + encrypted items will be automatically decrypted if they were decrypted + earlier. """ if not path.exists(self._config_path): print "Config file not found:", self._config_path @@ -236,7 +240,7 @@ class BotConfig(object): if choice.lower().startswith("y"): self._make_new() else: - exit(1) # TODO: raise an exception instead + raise NoConfigError() self._load() data = self._data @@ -255,16 +259,19 @@ class BotConfig(object): self._decrypt(node, nodes) def decrypt(self, node, *nodes): - """Use self._decryption_cipher to decrypt an object in our config tree. + """Decrypt an object in our config tree. + + :py:attr:`_decryption_cipher` is used as our key, retrieved using + :py:func:`~getpass.getpass` in :py:meth:`load` if it wasn't already + specified. If this is called when passwords are not encrypted (check + with :py:meth:`is_encrypted`), nothing will happen. We'll also keep + track of this node if :py:meth:`load` is called again (i.e. to reload) + and automatically decrypt it. - If this is called when passwords are not encrypted (check with - config.is_encrypted()), nothing will happen. We'll also keep track of - this node if config.load() is called again (i.e. to reload) and - automatically decrypt it. + Example usage:: - Example usage: - config.decrypt(config.irc, "frontend", "nickservPassword") - -> decrypts config.irc["frontend"]["nickservPassword"] + >>> config.decrypt(config.irc, "frontend", "nickservPassword") + # decrypts config.irc["frontend"]["nickservPassword"] """ self._decryptable_nodes.append((node, nodes)) if self.is_encrypted(): @@ -273,9 +280,8 @@ class BotConfig(object): def schedule(self, minute, hour, month_day, month, week_day): """Return a list of tasks scheduled to run at the specified time. - The schedule data comes from our config file's 'schedule' field, which - is stored as self._data["schedule"]. Call this function as - config.schedule(args). + The schedule data comes from our config file's ``schedule`` field, + which is stored as :py:attr:`self.data["schedule"] `. """ # Tasks to run this turn, each as a list of either [task_name, kwargs], # or just the task_name: diff --git a/earwigbot/exceptions.py b/earwigbot/exceptions.py index c11d74f..e3dbbbb 100644 --- a/earwigbot/exceptions.py +++ b/earwigbot/exceptions.py @@ -26,6 +26,7 @@ EarwigBot Exceptions This module contains all exceptions used by EarwigBot:: EarwigBotError + +-- NoConfigError +-- IRCError | +-- BrokenSocketError | +-- KwargParseError @@ -55,6 +56,13 @@ This module contains all exceptions used by EarwigBot:: class EarwigBotError(Exception): """Base exception class for errors in EarwigBot.""" +class NoConfigError(EarwigBotError): + """The bot cannot be run without a config file. + + This occurs if no config file exists, and the user said they did not want + one to be created. + """ + class IRCError(EarwigBotError): """Base exception class for errors in IRC-relation sections of the bot.""" diff --git a/earwigbot/managers.py b/earwigbot/managers.py index 9cee6b8..71184ae 100644 --- a/earwigbot/managers.py +++ b/earwigbot/managers.py @@ -34,21 +34,21 @@ __all__ = ["CommandManager", "TaskManager"] class _ResourceManager(object): """ - EarwigBot's Base Resource Manager - Resources are essentially objects dynamically loaded by the bot, both packaged with it (built-in resources) and created by users (plugins, aka custom resources). Currently, the only two types of resources are IRC commands and bot tasks. These are both loaded from two locations: the - earwigbot.commands and earwigbot.tasks packages, and the commands/ and - tasks/ directories within the bot's working directory. - - This class handles the low-level tasks of (re)loading resources via load(), - retrieving specific resources via get(), and iterating over all resources - via __iter__(). If iterating over resources, it is recommended to acquire - self.lock beforehand and release it afterwards (alternatively, wrap your - code in a `with` statement) so an attempt at reloading resources in another - thread won't disrupt your iteration. + :py:mod:`earwigbot.commands` and :py:mod:`earwigbot.tasks packages`, and + the :file:`commands/` and :file:`tasks/` directories within the bot's + working directory. + + This class handles the low-level tasks of (re)loading resources via + :py:meth:`load`, retrieving specific resources via :py:meth:`get`, and + iterating over all resources via :py:meth:`__iter__`. If iterating over + resources, it is recommended to acquire :py:attr:`self.lock ` + beforehand and release it afterwards (alternatively, wrap your code in a + ``with`` statement) so an attempt at reloading resources in another thread + won't disrupt your iteration. """ def __init__(self, bot, name, attribute, base): self.bot = bot @@ -62,6 +62,7 @@ class _ResourceManager(object): @property def lock(self): + """The resource access/modify lock.""" return self._resource_access_lock def __iter__(self): @@ -116,7 +117,7 @@ class _ResourceManager(object): processed.append(modname) def load(self): - """Load (or reload) all valid resources into self._resources.""" + """Load (or reload) all valid resources into :py:attr:`_resources`.""" name = self._resource_name # e.g. "commands" or "tasks" with self.lock: self._resources.clear() @@ -132,15 +133,14 @@ class _ResourceManager(object): def get(self, key): """Return the class instance associated with a certain resource. - Will raise KeyError if the resource (command or task) is not found. + Will raise :py:exc:`KeyError` if the resource (a command or task) is + not found. """ return self._resources[key] class CommandManager(_ResourceManager): """ - EarwigBot's IRC Command Manager - Manages (i.e., loads, reloads, and calls) IRC commands. """ def __init__(self, bot): @@ -164,7 +164,7 @@ class CommandManager(_ResourceManager): self.logger.exception(e.format(command.name)) def call(self, hook, data): - """Given a hook type and a Data object, respond appropriately.""" + """Respond to a hook type and a :py:class:`Data` object.""" self.lock.acquire() for command in self._resources.itervalues(): if hook in command.hooks and self._wrap_check(command, data): @@ -176,8 +176,6 @@ class CommandManager(_ResourceManager): class TaskManager(_ResourceManager): """ - EarwigBot's Bot Task Manager - Manages (i.e., loads, reloads, schedules, and runs) wiki bot tasks. """ def __init__(self, bot): @@ -197,8 +195,9 @@ class TaskManager(_ResourceManager): def start(self, task_name, **kwargs): """Start a given task in a new daemon thread, and return the thread. - kwargs are passed to task.run(). If the task is not found, None will be - returned. + kwargs are passed to :py:meth:`task.run() `. + If the task is not found, ``None`` will be returned an an error is + logged. """ msg = "Starting task '{0}' in a new thread" self.logger.info(msg.format(task_name)) diff --git a/earwigbot/util.py b/earwigbot/util.py index bac47e2..b75c453 100755 --- a/earwigbot/util.py +++ b/earwigbot/util.py @@ -22,8 +22,27 @@ # SOFTWARE. """ -This is EarwigBot's command-line utility, enabling you to easily start the -bot or run specific tasks. +usage: :command:`earwigbot [-h] [-v] [-d] [-q] [-t NAME] [PATH]` + +This is EarwigBot's command-line utility, enabling you to easily start the bot +or run specific tasks. + +.. glossary:: + +``PATH`` + path to the bot's working directory, which will be created if it doesn't + exist; current directory assumed if not specified +``-h``, ``--help`` + show this help message and exit +``-v``, ``--version`` + show program's version number and exit +``-d``, ``--debug`` + print all logs, including ``DEBUG``-level messages +``-q``, ``--quiet`` + don't print any logs except warnings and errors +``-t NAME``, ``--task NAME`` + given the name of a task, the bot will run it instead of the main bot and + then exit """ from argparse import ArgumentParser @@ -37,17 +56,23 @@ from earwigbot.bot import Bot __all__ = ["main"] def main(): + """Main entry point for the command-line utility.""" version = "EarwigBot v{0}".format(__version__) - parser = ArgumentParser(description=__doc__) + desc = """This is EarwigBot's command-line utility, enabling you to easily + start the bot or run specific tasks.""" + parser = ArgumentParser(description=desc) parser.add_argument("path", nargs="?", metavar="PATH", default=path.curdir, - help="path to the bot's working directory, which will be created if it doesn't exist; current directory assumed if not specified") + help="""path to the bot's working directory, which will + be created if it doesn't exist; current + directory assumed if not specified""") parser.add_argument("-v", "--version", action="version", version=version) parser.add_argument("-d", "--debug", action="store_true", help="print all logs, including DEBUG-level messages") parser.add_argument("-q", "--quiet", action="store_true", help="don't print any logs except warnings and errors") parser.add_argument("-t", "--task", metavar="NAME", - help="given the name of a task, the bot will run it instead of the main bot and then exit") + help="""given the name of a task, the bot will run it + instead of the main bot and then exit""") args = parser.parse_args() level = logging.INFO diff --git a/earwigbot/wiki/page.py b/earwigbot/wiki/page.py index f6ea257..a9bb5af 100644 --- a/earwigbot/wiki/page.py +++ b/earwigbot/wiki/page.py @@ -693,9 +693,8 @@ class Page(CopyrightMixin): """Add a new section to the bottom of the page. The arguments for this are the same as those for :py:meth:`edit`, but - instead of providing a summary, you provide a section title. - - Likewise, raised exceptions are the same as :py:meth:`edit`'s. + instead of providing a summary, you provide a section title. Likewise, + raised exceptions are the same as :py:meth:`edit`'s. This should create the page if it does not already exist, with just the new section as content. diff --git a/earwigbot/wiki/site.py b/earwigbot/wiki/site.py index 4b95e9c..42204cf 100644 --- a/earwigbot/wiki/site.py +++ b/earwigbot/wiki/site.py @@ -646,11 +646,10 @@ class Site(object): If *all* is ``False`` (default), we'll return the first name in the list, which is usually the localized version. Otherwise, we'll return - the entire list, which includes the canonical name. - - For example, this returns ``u"Wikipedia"`` if *ns_id* = ``4`` and - *all* = ``False`` on ``enwiki``; returns ``[u"Wikipedia", u"Project", - u"WP"]`` if *ns_id* = ``4`` and *all* is ``True``. + the entire list, which includes the canonical name. For example, this + returns ``u"Wikipedia"`` if *ns_id* = ``4`` and *all* is ``False`` on + ``enwiki``; returns ``[u"Wikipedia", u"Project", u"WP"]`` if *ns_id* = + ``4`` and *all* is ``True``. Raises :py:exc:`~earwigbot.exceptions.NamespaceNotFoundError` if the ID is not found.