@@ -7,7 +7,6 @@ earwigbot Package | |||||
.. automodule:: earwigbot.__init__ | .. automodule:: earwigbot.__init__ | ||||
:members: | :members: | ||||
:undoc-members: | :undoc-members: | ||||
:show-inheritance: | |||||
:mod:`bot` Module | :mod:`bot` Module | ||||
----------------- | ----------------- | ||||
@@ -15,7 +14,6 @@ earwigbot Package | |||||
.. automodule:: earwigbot.bot | .. automodule:: earwigbot.bot | ||||
:members: | :members: | ||||
:undoc-members: | :undoc-members: | ||||
:show-inheritance: | |||||
:mod:`config` Module | :mod:`config` Module | ||||
-------------------- | -------------------- | ||||
@@ -23,7 +21,6 @@ earwigbot Package | |||||
.. automodule:: earwigbot.config | .. automodule:: earwigbot.config | ||||
:members: | :members: | ||||
:undoc-members: | :undoc-members: | ||||
:show-inheritance: | |||||
:mod:`exceptions` Module | :mod:`exceptions` Module | ||||
------------------------ | ------------------------ | ||||
@@ -37,7 +34,7 @@ earwigbot Package | |||||
---------------------- | ---------------------- | ||||
.. automodule:: earwigbot.managers | .. automodule:: earwigbot.managers | ||||
:members: | |||||
:members: _ResourceManager, CommandManager, TaskManager | |||||
:undoc-members: | :undoc-members: | ||||
:show-inheritance: | :show-inheritance: | ||||
@@ -47,7 +44,6 @@ earwigbot Package | |||||
.. automodule:: earwigbot.util | .. automodule:: earwigbot.util | ||||
:members: | :members: | ||||
:undoc-members: | :undoc-members: | ||||
:show-inheritance: | |||||
Subpackages | Subpackages | ||||
----------- | ----------- | ||||
@@ -57,7 +57,7 @@ The most useful attributes are: | |||||
logged and used as the quit message when disconnecting from IRC. | logged and used as the quit message when disconnecting from IRC. | ||||
:py:class:`earwigbot.config.BotConfig` stores configuration information for the | :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`, | 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 | :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` | of the bot's :file:`config.yml` file. For example, if :file:`config.yml` | ||||
@@ -21,11 +21,12 @@ | |||||
# SOFTWARE. | # 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" | __author__ = "Ben Kurtovic" | ||||
@@ -40,18 +40,21 @@ class Bot(object): | |||||
EarwigBot has three components that can run independently of each other: an | EarwigBot has three components that can run independently of each other: an | ||||
IRC front-end, an IRC watcher, and a wiki scheduler. | 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. | 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. | 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. | 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): | 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 | This is thread-safe, and it will gracefully stop IRC components before | ||||
reloading anything. Note that you can safely reload commands or tasks | 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: | if msg: | ||||
self.logger.info('Restarting bot ("{0}")'.format(msg)) | self.logger.info('Restarting bot ("{0}")'.format(msg)) | ||||
@@ -180,7 +183,7 @@ class Bot(object): | |||||
def stop(self, msg=None): | def stop(self, msg=None): | ||||
"""Gracefully stop all bot components. | """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: | if msg: | ||||
self.logger.info('Stopping bot ("{0}")'.format(msg)) | self.logger.info('Stopping bot ("{0}")'.format(msg)) | ||||
@@ -29,36 +29,34 @@ from os import mkdir, path | |||||
from Crypto.Cipher import Blowfish | from Crypto.Cipher import Blowfish | ||||
import yaml | import yaml | ||||
from earwigbot.exceptions import NoConfigError | |||||
__all__ = ["BotConfig"] | __all__ = ["BotConfig"] | ||||
class BotConfig(object): | 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, | This handles all tasks involving reading and writing to our config file, | ||||
including encrypting and decrypting passwords and making a new config file | including encrypting and decrypting passwords and making a new config file | ||||
from scratch at the inital bot run. | 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): | def __init__(self, root_dir, level): | ||||
@@ -159,10 +157,12 @@ class BotConfig(object): | |||||
@property | @property | ||||
def root_dir(self): | def root_dir(self): | ||||
"""The bot's root directory containing its config file and more.""" | |||||
return self._root_dir | return self._root_dir | ||||
@property | @property | ||||
def logging_level(self): | def logging_level(self): | ||||
"""The minimum logging level for messages logged via stdout.""" | |||||
return self._logging_level | return self._logging_level | ||||
@logging_level.setter | @logging_level.setter | ||||
@@ -172,15 +172,17 @@ class BotConfig(object): | |||||
@property | @property | ||||
def path(self): | def path(self): | ||||
"""The path to the bot's config file.""" | |||||
return self._config_path | return self._config_path | ||||
@property | @property | ||||
def log_dir(self): | def log_dir(self): | ||||
"""The directory containing the bot's logs.""" | |||||
return self._log_dir | return self._log_dir | ||||
@property | @property | ||||
def data(self): | def data(self): | ||||
"""The entire config file.""" | |||||
"""The entire config file as a decoded JSON object.""" | |||||
return self._data | return self._data | ||||
@property | @property | ||||
@@ -209,11 +211,11 @@ class BotConfig(object): | |||||
return self._metadata | return self._metadata | ||||
def is_loaded(self): | 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 | return self._data is not None | ||||
def is_encrypted(self): | 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) | return self.metadata.get("encryptPasswords", False) | ||||
def load(self): | def load(self): | ||||
@@ -223,12 +225,14 @@ class BotConfig(object): | |||||
user. If there is no config file at all, offer to make one, otherwise | user. If there is no config file at all, offer to make one, otherwise | ||||
exit. | 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): | if not path.exists(self._config_path): | ||||
print "Config file not found:", self._config_path | print "Config file not found:", self._config_path | ||||
@@ -236,7 +240,7 @@ class BotConfig(object): | |||||
if choice.lower().startswith("y"): | if choice.lower().startswith("y"): | ||||
self._make_new() | self._make_new() | ||||
else: | else: | ||||
exit(1) # TODO: raise an exception instead | |||||
raise NoConfigError() | |||||
self._load() | self._load() | ||||
data = self._data | data = self._data | ||||
@@ -255,16 +259,19 @@ class BotConfig(object): | |||||
self._decrypt(node, nodes) | self._decrypt(node, nodes) | ||||
def decrypt(self, 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)) | self._decryptable_nodes.append((node, nodes)) | ||||
if self.is_encrypted(): | if self.is_encrypted(): | ||||
@@ -273,9 +280,8 @@ class BotConfig(object): | |||||
def schedule(self, minute, hour, month_day, month, week_day): | def schedule(self, minute, hour, month_day, month, week_day): | ||||
"""Return a list of tasks scheduled to run at the specified time. | """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], | # Tasks to run this turn, each as a list of either [task_name, kwargs], | ||||
# or just the task_name: | # or just the task_name: | ||||
@@ -26,6 +26,7 @@ EarwigBot Exceptions | |||||
This module contains all exceptions used by EarwigBot:: | This module contains all exceptions used by EarwigBot:: | ||||
EarwigBotError | EarwigBotError | ||||
+-- NoConfigError | |||||
+-- IRCError | +-- IRCError | ||||
| +-- BrokenSocketError | | +-- BrokenSocketError | ||||
| +-- KwargParseError | | +-- KwargParseError | ||||
@@ -55,6 +56,13 @@ This module contains all exceptions used by EarwigBot:: | |||||
class EarwigBotError(Exception): | class EarwigBotError(Exception): | ||||
"""Base exception class for errors in EarwigBot.""" | """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): | class IRCError(EarwigBotError): | ||||
"""Base exception class for errors in IRC-relation sections of the bot.""" | """Base exception class for errors in IRC-relation sections of the bot.""" | ||||
@@ -34,21 +34,21 @@ __all__ = ["CommandManager", "TaskManager"] | |||||
class _ResourceManager(object): | class _ResourceManager(object): | ||||
""" | """ | ||||
EarwigBot's Base Resource Manager | |||||
Resources are essentially objects dynamically loaded by the bot, both | Resources are essentially objects dynamically loaded by the bot, both | ||||
packaged with it (built-in resources) and created by users (plugins, aka | packaged with it (built-in resources) and created by users (plugins, aka | ||||
custom resources). Currently, the only two types of resources are IRC | custom resources). Currently, the only two types of resources are IRC | ||||
commands and bot tasks. These are both loaded from two locations: the | 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): | def __init__(self, bot, name, attribute, base): | ||||
self.bot = bot | self.bot = bot | ||||
@@ -62,6 +62,7 @@ class _ResourceManager(object): | |||||
@property | @property | ||||
def lock(self): | def lock(self): | ||||
"""The resource access/modify lock.""" | |||||
return self._resource_access_lock | return self._resource_access_lock | ||||
def __iter__(self): | def __iter__(self): | ||||
@@ -116,7 +117,7 @@ class _ResourceManager(object): | |||||
processed.append(modname) | processed.append(modname) | ||||
def load(self): | 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" | name = self._resource_name # e.g. "commands" or "tasks" | ||||
with self.lock: | with self.lock: | ||||
self._resources.clear() | self._resources.clear() | ||||
@@ -132,15 +133,14 @@ class _ResourceManager(object): | |||||
def get(self, key): | def get(self, key): | ||||
"""Return the class instance associated with a certain resource. | """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] | return self._resources[key] | ||||
class CommandManager(_ResourceManager): | class CommandManager(_ResourceManager): | ||||
""" | """ | ||||
EarwigBot's IRC Command Manager | |||||
Manages (i.e., loads, reloads, and calls) IRC commands. | Manages (i.e., loads, reloads, and calls) IRC commands. | ||||
""" | """ | ||||
def __init__(self, bot): | def __init__(self, bot): | ||||
@@ -164,7 +164,7 @@ class CommandManager(_ResourceManager): | |||||
self.logger.exception(e.format(command.name)) | self.logger.exception(e.format(command.name)) | ||||
def call(self, hook, data): | 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() | self.lock.acquire() | ||||
for command in self._resources.itervalues(): | for command in self._resources.itervalues(): | ||||
if hook in command.hooks and self._wrap_check(command, data): | if hook in command.hooks and self._wrap_check(command, data): | ||||
@@ -176,8 +176,6 @@ class CommandManager(_ResourceManager): | |||||
class TaskManager(_ResourceManager): | class TaskManager(_ResourceManager): | ||||
""" | """ | ||||
EarwigBot's Bot Task Manager | |||||
Manages (i.e., loads, reloads, schedules, and runs) wiki bot tasks. | Manages (i.e., loads, reloads, schedules, and runs) wiki bot tasks. | ||||
""" | """ | ||||
def __init__(self, bot): | def __init__(self, bot): | ||||
@@ -197,8 +195,9 @@ class TaskManager(_ResourceManager): | |||||
def start(self, task_name, **kwargs): | def start(self, task_name, **kwargs): | ||||
"""Start a given task in a new daemon thread, and return the thread. | """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" | msg = "Starting task '{0}' in a new thread" | ||||
self.logger.info(msg.format(task_name)) | self.logger.info(msg.format(task_name)) | ||||
@@ -22,8 +22,27 @@ | |||||
# SOFTWARE. | # 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 | from argparse import ArgumentParser | ||||
@@ -37,17 +56,23 @@ from earwigbot.bot import Bot | |||||
__all__ = ["main"] | __all__ = ["main"] | ||||
def main(): | def main(): | ||||
"""Main entry point for the command-line utility.""" | |||||
version = "EarwigBot v{0}".format(__version__) | 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, | 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("-v", "--version", action="version", version=version) | ||||
parser.add_argument("-d", "--debug", action="store_true", | parser.add_argument("-d", "--debug", action="store_true", | ||||
help="print all logs, including DEBUG-level messages") | help="print all logs, including DEBUG-level messages") | ||||
parser.add_argument("-q", "--quiet", action="store_true", | parser.add_argument("-q", "--quiet", action="store_true", | ||||
help="don't print any logs except warnings and errors") | help="don't print any logs except warnings and errors") | ||||
parser.add_argument("-t", "--task", metavar="NAME", | 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() | args = parser.parse_args() | ||||
level = logging.INFO | level = logging.INFO | ||||
@@ -693,9 +693,8 @@ class Page(CopyrightMixin): | |||||
"""Add a new section to the bottom of the page. | """Add a new section to the bottom of the page. | ||||
The arguments for this are the same as those for :py:meth:`edit`, but | 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 | This should create the page if it does not already exist, with just the | ||||
new section as content. | new section as content. | ||||
@@ -646,11 +646,10 @@ class Site(object): | |||||
If *all* is ``False`` (default), we'll return the first name in the | If *all* is ``False`` (default), we'll return the first name in the | ||||
list, which is usually the localized version. Otherwise, we'll return | 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 | Raises :py:exc:`~earwigbot.exceptions.NamespaceNotFoundError` if the ID | ||||
is not found. | is not found. | ||||