Selaa lähdekoodia

Better docstrings for a bunch of modules

tags/v0.1^2
Ben Kurtovic 12 vuotta sitten
vanhempi
commit
6450d99a0f
10 muutettua tiedostoa jossa 135 lisäystä ja 99 poistoa
  1. +1
    -5
      docs/api/earwigbot.rst
  2. +1
    -1
      docs/customizing.rst
  3. +5
    -4
      earwigbot/__init__.py
  4. +17
    -14
      earwigbot/bot.py
  5. +48
    -42
      earwigbot/config.py
  6. +8
    -0
      earwigbot/exceptions.py
  7. +19
    -20
      earwigbot/managers.py
  8. +30
    -5
      earwigbot/util.py
  9. +2
    -3
      earwigbot/wiki/page.py
  10. +4
    -5
      earwigbot/wiki/site.py

+ 1
- 5
docs/api/earwigbot.rst Näytä tiedosto

@@ -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
-----------


+ 1
- 1
docs/customizing.rst Näytä tiedosto

@@ -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`


+ 5
- 4
earwigbot/__init__.py Näytä tiedosto

@@ -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 <https://github.com/earwig/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
<http://packages.python.org/earwigbot>`_.
"""

__author__ = "Ben Kurtovic"


+ 17
- 14
earwigbot/bot.py Näytä tiedosto

@@ -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 <earwigbot.managers.TaskManager.start>`, and
sites can be loaded from the wiki toolset with :py:meth:`bot.wiki.get_site
<earwigbot.wiki.sitesdb.SitesDB.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))


+ 48
- 42
earwigbot/config.py Näytä tiedosto

@@ -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"] <data>`.
"""
# Tasks to run this turn, each as a list of either [task_name, kwargs],
# or just the task_name:


+ 8
- 0
earwigbot/exceptions.py Näytä tiedosto

@@ -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."""



+ 19
- 20
earwigbot/managers.py Näytä tiedosto

@@ -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 <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() <earwigbot.tasks.BaseTask>`.
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))


+ 30
- 5
earwigbot/util.py Näytä tiedosto

@@ -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


+ 2
- 3
earwigbot/wiki/page.py Näytä tiedosto

@@ -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.


+ 4
- 5
earwigbot/wiki/site.py Näytä tiedosto

@@ -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.


Ladataan…
Peruuta
Tallenna