@@ -49,4 +49,12 @@ if not __release__: | |||||
finally: | finally: | ||||
del _add_git_commit_id_to_version_string | del _add_git_commit_id_to_version_string | ||||
from earwigbot import bot, commands, config, irc, managers, tasks, util, wiki | |||||
from earwigbot import bot | |||||
from earwigbot import commands | |||||
from earwigbot import config | |||||
from earwigbot import exceptions | |||||
from earwigbot import irc | |||||
from earwigbot import managers | |||||
from earwigbot import tasks | |||||
from earwigbot import util | |||||
from earwigbot import wiki |
@@ -21,34 +21,38 @@ | |||||
# SOFTWARE. | # SOFTWARE. | ||||
""" | """ | ||||
EarwigBot's Wiki Toolset: Exceptions | |||||
This module contains all exceptions used by the wiki package. There are a lot: | |||||
-- SiteNotFoundError | |||||
-- SiteAPIError | |||||
-- LoginError | |||||
-- NamespaceNotFoundError | |||||
-- PageNotFoundError | |||||
-- InvalidPageError | |||||
-- RedirectError | |||||
-- UserNotFoundError | |||||
-- EditError | |||||
-- PermissionsError | |||||
-- EditConflictError | |||||
-- NoContentError | |||||
-- ContentTooBigError | |||||
-- SpamDetectedError | |||||
-- FilteredError | |||||
-- SQLError | |||||
-- CopyvioCheckError | |||||
-- UnknownSearchEngineError | |||||
-- UnsupportedSearchEngineError | |||||
-- SearchQueryError | |||||
EarwigBot Exceptions | |||||
This module contains all exceptions used by EarwigBot:: | |||||
EarwigBotError | |||||
+-- WikiToolsetError | |||||
+-- SiteNotFoundError | |||||
+-- SiteAPIError | |||||
+-- LoginError | |||||
+-- NamespaceNotFoundError | |||||
+-- PageNotFoundError | |||||
+-- InvalidPageError | |||||
+-- RedirectError | |||||
+-- UserNotFoundError | |||||
+-- EditError | |||||
| +-- PermissionsError | |||||
| +-- EditConflictError | |||||
| +-- NoContentError | |||||
| +-- ContentTooBigError | |||||
| +-- SpamDetectedError | |||||
| +-- FilteredError | |||||
+-- SQLError | |||||
+-- CopyvioCheckError | |||||
+-- UnknownSearchEngineError | |||||
+-- UnsupportedSearchEngineError | |||||
+-- SearchQueryError | |||||
""" | """ | ||||
class WikiToolsetError(Exception): | |||||
class EarwigBotErorr(Exception): | |||||
"""Base exception class for errors in EarwigBot.""" | |||||
class WikiToolsetError(EarwigBotErorr): | |||||
"""Base exception class for errors in the Wiki Toolset.""" | """Base exception class for errors in the Wiki Toolset.""" | ||||
class SiteNotFoundError(WikiToolsetError): | class SiteNotFoundError(WikiToolsetError): |
@@ -37,7 +37,6 @@ Category, and User) needs. | |||||
from earwigbot.wiki.category import * | from earwigbot.wiki.category import * | ||||
from earwigbot.wiki.constants import * | from earwigbot.wiki.constants import * | ||||
from earwigbot.wiki.exceptions import * | |||||
from earwigbot.wiki.page import * | from earwigbot.wiki.page import * | ||||
from earwigbot.wiki.site import * | from earwigbot.wiki.site import * | ||||
from earwigbot.wiki.sitesdb import * | from earwigbot.wiki.sitesdb import * | ||||
@@ -35,7 +35,8 @@ earwigbot.wiki (e.g. `earwigbot.wiki.USER_AGENT`). | |||||
# Default User Agent when making API queries: | # Default User Agent when making API queries: | ||||
from earwigbot import __version__ as _v | from earwigbot import __version__ as _v | ||||
from platform import python_version as _p | from platform import python_version as _p | ||||
USER_AGENT = "EarwigBot/{0} (Python/{1}; https://github.com/earwig/earwigbot)".format(_v, _p()) | |||||
USER_AGENT = "EarwigBot/{0} (Python/{1}; https://github.com/earwig/earwigbot)" | |||||
USER_AGENT = USER_AGENT.format(_v, _p()) | |||||
del _v, _p | del _v, _p | ||||
# Default namespace IDs: | # Default namespace IDs: | ||||
@@ -35,7 +35,7 @@ try: | |||||
except ImportError: | except ImportError: | ||||
oauth = None | oauth = None | ||||
from earwigbot.wiki.exceptions import * | |||||
from earwigbot.exceptions import * | |||||
class _CopyvioCheckResult(object): | class _CopyvioCheckResult(object): | ||||
def __init__(self, violation, confidence, url, queries, article, chains): | def __init__(self, violation, confidence, url, queries, article, chains): | ||||
@@ -25,8 +25,8 @@ import re | |||||
from time import gmtime, strftime | from time import gmtime, strftime | ||||
from urllib import quote | from urllib import quote | ||||
from earwigbot import exceptions | |||||
from earwigbot.wiki.copyright import CopyrightMixin | from earwigbot.wiki.copyright import CopyrightMixin | ||||
from earwigbot.wiki.exceptions import * | |||||
__all__ = ["Page"] | __all__ = ["Page"] | ||||
@@ -132,7 +132,7 @@ class Page(CopyrightMixin): | |||||
""" | """ | ||||
if self._exists == 1: | if self._exists == 1: | ||||
e = "Page '{0}' is invalid.".format(self._title) | e = "Page '{0}' is invalid.".format(self._title) | ||||
raise InvalidPageError(e) | |||||
raise exceptions.InvalidPageError(e) | |||||
def _force_existence(self): | def _force_existence(self): | ||||
"""Used to ensure that our page exists. | """Used to ensure that our page exists. | ||||
@@ -144,7 +144,7 @@ class Page(CopyrightMixin): | |||||
self._force_validity() | self._force_validity() | ||||
if self._exists == 2: | if self._exists == 2: | ||||
e = "Page '{0}' does not exist.".format(self._title) | e = "Page '{0}' does not exist.".format(self._title) | ||||
raise PageNotFoundError(e) | |||||
raise exceptions.PageNotFoundError(e) | |||||
def _load_wrapper(self): | def _load_wrapper(self): | ||||
"""Calls _load_attributes() and follows redirects if we're supposed to. | """Calls _load_attributes() and follows redirects if we're supposed to. | ||||
@@ -278,8 +278,8 @@ class Page(CopyrightMixin): | |||||
self._load_attributes() | self._load_attributes() | ||||
if not self._token: | if not self._token: | ||||
e = "You don't have permission to edit this page." | e = "You don't have permission to edit this page." | ||||
raise PermissionsError(e) | |||||
raise exceptions.PermissionsError(e) | |||||
# Weed out invalid pages before we get too far: | # Weed out invalid pages before we get too far: | ||||
self._force_validity() | self._force_validity() | ||||
@@ -310,7 +310,7 @@ class Page(CopyrightMixin): | |||||
try: | try: | ||||
assertion = result["edit"]["assert"] | assertion = result["edit"]["assert"] | ||||
except KeyError: | except KeyError: | ||||
raise EditError(result["edit"]) | |||||
raise exceptions.EditError(result["edit"]) | |||||
self._handle_assert_edit(assertion, params, tries) | self._handle_assert_edit(assertion, params, tries) | ||||
def _build_edit_params(self, text, summary, minor, bot, force, section, | def _build_edit_params(self, text, summary, minor, bot, force, section, | ||||
@@ -353,13 +353,13 @@ class Page(CopyrightMixin): | |||||
""" | """ | ||||
if error.code in ["noedit", "cantcreate", "protectedtitle", | if error.code in ["noedit", "cantcreate", "protectedtitle", | ||||
"noimageredirect"]: | "noimageredirect"]: | ||||
raise PermissionsError(error.info) | |||||
raise exceptions.PermissionsError(error.info) | |||||
elif error.code in ["noedit-anon", "cantcreate-anon", | elif error.code in ["noedit-anon", "cantcreate-anon", | ||||
"noimageredirect-anon"]: | "noimageredirect-anon"]: | ||||
if not all(self._site._login_info): | if not all(self._site._login_info): | ||||
# Insufficient login info: | # Insufficient login info: | ||||
raise PermissionsError(error.info) | |||||
raise exceptions.PermissionsError(error.info) | |||||
if tries == 0: | if tries == 0: | ||||
# We have login info; try to login: | # We have login info; try to login: | ||||
self._site._login(self._site._login_info) | self._site._login(self._site._login_info) | ||||
@@ -368,28 +368,28 @@ class Page(CopyrightMixin): | |||||
else: | else: | ||||
# We already tried to log in and failed! | # We already tried to log in and failed! | ||||
e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug." | e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug." | ||||
raise LoginError(e) | |||||
raise exceptions.LoginError(e) | |||||
elif error.code in ["editconflict", "pagedeleted", "articleexists"]: | elif error.code in ["editconflict", "pagedeleted", "articleexists"]: | ||||
# These attributes are now invalidated: | # These attributes are now invalidated: | ||||
self._content = None | self._content = None | ||||
self._basetimestamp = None | self._basetimestamp = None | ||||
self._exists = 0 | self._exists = 0 | ||||
raise EditConflictError(error.info) | |||||
raise exceptions.EditConflictError(error.info) | |||||
elif error.code in ["emptypage", "emptynewsection"]: | elif error.code in ["emptypage", "emptynewsection"]: | ||||
raise NoContentError(error.info) | |||||
raise exceptions.NoContentError(error.info) | |||||
elif error.code == "contenttoobig": | elif error.code == "contenttoobig": | ||||
raise ContentTooBigError(error.info) | |||||
raise exceptions.ContentTooBigError(error.info) | |||||
elif error.code == "spamdetected": | elif error.code == "spamdetected": | ||||
raise SpamDetectedError(error.info) | |||||
raise exceptions.SpamDetectedError(error.info) | |||||
elif error.code == "filtered": | elif error.code == "filtered": | ||||
raise FilteredError(error.info) | |||||
raise exceptions.FilteredError(error.info) | |||||
raise EditError(": ".join((error.code, error.info))) | |||||
raise exceptions.EditError(": ".join((error.code, error.info))) | |||||
def _handle_assert_edit(self, assertion, params, tries): | def _handle_assert_edit(self, assertion, params, tries): | ||||
"""If we can't edit due to a failed AssertEdit assertion, handle that. | """If we can't edit due to a failed AssertEdit assertion, handle that. | ||||
@@ -401,7 +401,7 @@ class Page(CopyrightMixin): | |||||
if not all(self._site._login_info): | if not all(self._site._login_info): | ||||
# Insufficient login info: | # Insufficient login info: | ||||
e = "AssertEdit: user assertion failed, and no login info was provided." | e = "AssertEdit: user assertion failed, and no login info was provided." | ||||
raise PermissionsError(e) | |||||
raise exceptions.PermissionsError(e) | |||||
if tries == 0: | if tries == 0: | ||||
# We have login info; try to login: | # We have login info; try to login: | ||||
self._site._login(self._site._login_info) | self._site._login(self._site._login_info) | ||||
@@ -410,15 +410,15 @@ class Page(CopyrightMixin): | |||||
else: | else: | ||||
# We already tried to log in and failed! | # We already tried to log in and failed! | ||||
e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug." | e = "Although we should be logged in, we are not. This may be a cookie problem or an odd bug." | ||||
raise LoginError(e) | |||||
raise exceptions.LoginError(e) | |||||
elif assertion == "bot": | elif assertion == "bot": | ||||
e = "AssertEdit: bot assertion failed; we don't have a bot flag!" | e = "AssertEdit: bot assertion failed; we don't have a bot flag!" | ||||
raise PermissionsError(e) | |||||
raise exceptions.PermissionsError(e) | |||||
# Unknown assertion, maybe "true", "false", or "exists": | # Unknown assertion, maybe "true", "false", or "exists": | ||||
e = "AssertEdit: assertion '{0}' failed.".format(assertion) | e = "AssertEdit: assertion '{0}' failed.".format(assertion) | ||||
raise PermissionsError(e) | |||||
raise exceptions.PermissionsError(e) | |||||
def title(self, force=False): | def title(self, force=False): | ||||
"""Returns the Page's title, or pagename. | """Returns the Page's title, or pagename. | ||||
@@ -570,7 +570,7 @@ class Page(CopyrightMixin): | |||||
if self._namespace < 0: | if self._namespace < 0: | ||||
ns = self._site.namespace_id_to_name(self._namespace) | ns = self._site.namespace_id_to_name(self._namespace) | ||||
e = "Pages in the {0} namespace can't have talk pages.".format(ns) | e = "Pages in the {0} namespace can't have talk pages.".format(ns) | ||||
raise InvalidPageError(e) | |||||
raise exceptions.InvalidPageError(e) | |||||
if self._is_talkpage: | if self._is_talkpage: | ||||
new_ns = self._namespace - 1 | new_ns = self._namespace - 1 | ||||
@@ -650,7 +650,7 @@ class Page(CopyrightMixin): | |||||
return re.findall(self.re_redirect, content, flags=re.I)[0] | return re.findall(self.re_redirect, content, flags=re.I)[0] | ||||
except IndexError: | except IndexError: | ||||
e = "The page does not appear to have a redirect target." | e = "The page does not appear to have a redirect target." | ||||
raise RedirectError(e) | |||||
raise exceptions.RedirectError(e) | |||||
def edit(self, text, summary, minor=False, bot=True, force=False): | def edit(self, text, summary, minor=False, bot=True, force=False): | ||||
"""Replaces the page's content or creates a new page. | """Replaces the page's content or creates a new page. | ||||
@@ -38,9 +38,9 @@ try: | |||||
except ImportError: | except ImportError: | ||||
oursql = None | oursql = None | ||||
from earwigbot import exceptions | |||||
from earwigbot.wiki import constants | |||||
from earwigbot.wiki.category import Category | from earwigbot.wiki.category import Category | ||||
from earwigbot.wiki.constants import * | |||||
from earwigbot.wiki.exceptions import * | |||||
from earwigbot.wiki.page import Page | from earwigbot.wiki.page import Page | ||||
from earwigbot.wiki.user import User | from earwigbot.wiki.user import User | ||||
@@ -128,7 +128,7 @@ class Site(object): | |||||
else: | else: | ||||
self._cookiejar = CookieJar() | self._cookiejar = CookieJar() | ||||
if not user_agent: | if not user_agent: | ||||
user_agent = USER_AGENT # Set default UA from wiki.constants | |||||
user_agent = constants.USER_AGENT # Set default UA | |||||
self._opener = build_opener(HTTPCookieProcessor(self._cookiejar)) | self._opener = build_opener(HTTPCookieProcessor(self._cookiejar)) | ||||
self._opener.addheaders = [("User-Agent", user_agent), | self._opener.addheaders = [("User-Agent", user_agent), | ||||
("Accept-Encoding", "gzip")] | ("Accept-Encoding", "gzip")] | ||||
@@ -232,7 +232,7 @@ class Site(object): | |||||
e = e.format(error.code) | e = e.format(error.code) | ||||
else: | else: | ||||
e = "API query failed." | e = "API query failed." | ||||
raise SiteAPIError(e) | |||||
raise exceptions.SiteAPIError(e) | |||||
result = response.read() | result = response.read() | ||||
if response.headers.get("Content-Encoding") == "gzip": | if response.headers.get("Content-Encoding") == "gzip": | ||||
@@ -246,7 +246,7 @@ class Site(object): | |||||
"""Given API query params, return the URL to query and POST data.""" | """Given API query params, return the URL to query and POST data.""" | ||||
if not self._base_url or self._script_path is None: | if not self._base_url or self._script_path is None: | ||||
e = "Tried to do an API query, but no API URL is known." | e = "Tried to do an API query, but no API URL is known." | ||||
raise SiteAPIError(e) | |||||
raise exceptions.SiteAPIError(e) | |||||
base_url = self._base_url | base_url = self._base_url | ||||
if base_url.startswith("//"): # Protocol-relative URLs from 1.18 | if base_url.startswith("//"): # Protocol-relative URLs from 1.18 | ||||
@@ -271,7 +271,7 @@ class Site(object): | |||||
res = loads(result) # Try to parse as a JSON object | res = loads(result) # Try to parse as a JSON object | ||||
except ValueError: | except ValueError: | ||||
e = "API query failed: JSON could not be decoded." | e = "API query failed: JSON could not be decoded." | ||||
raise SiteAPIError(e) | |||||
raise exceptions.SiteAPIError(e) | |||||
try: | try: | ||||
code = res["error"]["code"] | code = res["error"]["code"] | ||||
@@ -282,7 +282,7 @@ class Site(object): | |||||
if code == "maxlag": # We've been throttled by the server | if code == "maxlag": # We've been throttled by the server | ||||
if tries >= self._max_retries: | if tries >= self._max_retries: | ||||
e = "Maximum number of retries reached ({0})." | e = "Maximum number of retries reached ({0})." | ||||
raise SiteAPIError(e.format(self._max_retries)) | |||||
raise exceptions.SiteAPIError(e.format(self._max_retries)) | |||||
tries += 1 | tries += 1 | ||||
msg = 'Server says "{0}"; retrying in {1} seconds ({2}/{3})' | msg = 'Server says "{0}"; retrying in {1} seconds ({2}/{3})' | ||||
self._logger.info(msg.format(info, wait, tries, self._max_retries)) | self._logger.info(msg.format(info, wait, tries, self._max_retries)) | ||||
@@ -290,7 +290,7 @@ class Site(object): | |||||
return self._api_query(params, tries=tries, wait=wait*3) | return self._api_query(params, tries=tries, wait=wait*3) | ||||
else: # Some unknown error occurred | else: # Some unknown error occurred | ||||
e = 'API query failed: got error "{0}"; server says: "{1}".' | e = 'API query failed: got error "{0}"; server says: "{1}".' | ||||
error = SiteAPIError(e.format(code, info)) | |||||
error = earwigbot.SiteAPIError(e.format(code, info)) | |||||
error.code, error.info = code, info | error.code, error.info = code, info | ||||
raise error | raise error | ||||
@@ -491,7 +491,7 @@ class Site(object): | |||||
e = "The given password is incorrect." | e = "The given password is incorrect." | ||||
else: | else: | ||||
e = "Couldn't login; server says '{0}'.".format(res) | e = "Couldn't login; server says '{0}'.".format(res) | ||||
raise LoginError(e) | |||||
raise exceptions.LoginError(e) | |||||
def _logout(self): | def _logout(self): | ||||
"""Safely logout through the API. | """Safely logout through the API. | ||||
@@ -518,7 +518,7 @@ class Site(object): | |||||
""" | """ | ||||
if not oursql: | if not oursql: | ||||
e = "Module 'oursql' is required for SQL queries." | e = "Module 'oursql' is required for SQL queries." | ||||
raise SQLError(e) | |||||
raise exceptions.SQLError(e) | |||||
args = self._sql_data | args = self._sql_data | ||||
for key, value in kwargs.iteritems(): | for key, value in kwargs.iteritems(): | ||||
@@ -638,7 +638,7 @@ class Site(object): | |||||
return self._namespaces[ns_id][0] | return self._namespaces[ns_id][0] | ||||
except KeyError: | except KeyError: | ||||
e = "There is no namespace with id {0}.".format(ns_id) | e = "There is no namespace with id {0}.".format(ns_id) | ||||
raise NamespaceNotFoundError(e) | |||||
raise exceptions.NamespaceNotFoundError(e) | |||||
def namespace_name_to_id(self, name): | def namespace_name_to_id(self, name): | ||||
"""Given a namespace name, returns the associated ID. | """Given a namespace name, returns the associated ID. | ||||
@@ -655,7 +655,7 @@ class Site(object): | |||||
return ns_id | return ns_id | ||||
e = "There is no namespace with name '{0}'.".format(name) | e = "There is no namespace with name '{0}'.".format(name) | ||||
raise NamespaceNotFoundError(e) | |||||
raise exceptions.NamespaceNotFoundError(e) | |||||
def get_page(self, title, follow_redirects=False): | def get_page(self, title, follow_redirects=False): | ||||
"""Returns a Page object for the given title (pagename). | """Returns a Page object for the given title (pagename). | ||||
@@ -667,7 +667,7 @@ class Site(object): | |||||
Note that this doesn't do any direct checks for existence or | Note that this doesn't do any direct checks for existence or | ||||
redirect-following - Page's methods provide that. | redirect-following - Page's methods provide that. | ||||
""" | """ | ||||
prefixes = self.namespace_id_to_name(NS_CATEGORY, all=True) | |||||
prefixes = self.namespace_id_to_name(constants.NS_CATEGORY, all=True) | |||||
prefix = title.split(":", 1)[0] | prefix = title.split(":", 1)[0] | ||||
if prefix != title: # Avoid a page that is simply "Category" | if prefix != title: # Avoid a page that is simply "Category" | ||||
if prefix in prefixes: | if prefix in prefixes: | ||||
@@ -680,7 +680,7 @@ class Site(object): | |||||
`catname` should be given *without* a namespace prefix. This method is | `catname` should be given *without* a namespace prefix. This method is | ||||
really just shorthand for get_page("Category:" + catname). | really just shorthand for get_page("Category:" + catname). | ||||
""" | """ | ||||
prefix = self.namespace_id_to_name(NS_CATEGORY) | |||||
prefix = self.namespace_id_to_name(constants.NS_CATEGORY) | |||||
pagename = ':'.join((prefix, catname)) | pagename = ':'.join((prefix, catname)) | ||||
return Category(self, pagename, follow_redirects) | return Category(self, pagename, follow_redirects) | ||||
@@ -28,7 +28,7 @@ import stat | |||||
import sqlite3 as sqlite | import sqlite3 as sqlite | ||||
from earwigbot import __version__ | from earwigbot import __version__ | ||||
from earwigbot.wiki.exceptions import SiteNotFoundError | |||||
from earwigbot.exceptions import SiteNotFoundError | |||||
from earwigbot.wiki.site import Site | from earwigbot.wiki.site import Site | ||||
__all__ = ["SitesDB"] | __all__ = ["SitesDB"] | ||||
@@ -22,8 +22,8 @@ | |||||
from time import gmtime, strptime | from time import gmtime, strptime | ||||
from earwigbot.wiki.constants import * | |||||
from earwigbot.wiki.exceptions import UserNotFoundError | |||||
from earwigbot.exceptions import UserNotFoundError | |||||
from earwigbot.wiki import constants | |||||
from earwigbot.wiki.page import Page | from earwigbot.wiki.page import Page | ||||
__all__ = ["User"] | __all__ = ["User"] | ||||
@@ -252,7 +252,7 @@ class User(object): | |||||
No checks are made to see if it exists or not. Proper site namespace | No checks are made to see if it exists or not. Proper site namespace | ||||
conventions are followed. | conventions are followed. | ||||
""" | """ | ||||
prefix = self._site.namespace_id_to_name(NS_USER) | |||||
prefix = self._site.namespace_id_to_name(constants.NS_USER) | |||||
pagename = ':'.join((prefix, self._name)) | pagename = ':'.join((prefix, self._name)) | ||||
return Page(self._site, pagename) | return Page(self._site, pagename) | ||||
@@ -262,6 +262,6 @@ class User(object): | |||||
No checks are made to see if it exists or not. Proper site namespace | No checks are made to see if it exists or not. Proper site namespace | ||||
conventions are followed. | conventions are followed. | ||||
""" | """ | ||||
prefix = self._site.namespace_id_to_name(NS_USER_TALK) | |||||
prefix = self._site.namespace_id_to_name(constants.NS_USER_TALK) | |||||
pagename = ':'.join((prefix, self._name)) | pagename = ':'.join((prefix, self._name)) | ||||
return Page(self._site, pagename) | return Page(self._site, pagename) |