XZise has submitted this change and it was merged.
Change subject: Page save related exception hierarchy
......................................................................
Page save related exception hierarchy
All page save related exceptions are now subclasses of the new
PageSaveRelatedError, and PageNotSaved now is an alias of
PageSaveRelatedError.
Exceptions have been added for the known API error codes encountered
by Site.editpage that are errors specific to the page being edited,
and all page related save exceptions are re-raised to be handled by
scripts.
Scripts may continue to catch specific exceptions to customise
actions or errors, however scripts now only need to catch one exception
(PageNotSaved) to avoid halting on issues relating to a specific page.
PageNotSaved has been moved to last in the exception handling
of four scripts when it was previously before another exception
which is now a subclass of PageNotSaved.
Added edit_failure_tests to check exceptions are raised.
Bug: 55264
Bug: 67276
Change-Id: Id8b8c8055100329e7844b5db6500f71bbb1673f7
---
M pywikibot/__init__.py
M pywikibot/exceptions.py
M pywikibot/page.py
M pywikibot/site.py
M scripts/add_text.py
M scripts/blockpageschecker.py
M scripts/reflinks.py
M scripts/replace.py
A tests/edit_failure_tests.py
9 files changed, 236 insertions(+), 68 deletions(-)
Approvals:
XZise: Looks good to me, approved
diff --git a/pywikibot/__init__.py b/pywikibot/__init__.py
index 0228e79..184d875 100644
--- a/pywikibot/__init__.py
+++ b/pywikibot/__init__.py
@@ -37,7 +37,9 @@
Error, InvalidTitle, BadTitle, NoPage, SectionError,
NoSuchSite, NoUsername, UserBlocked,
PageRelatedError, IsRedirectPage, IsNotRedirectPage,
- PageNotSaved, LockedPage, EditConflict,
+ PageSaveRelatedError, PageNotSaved, OtherPageSaveError,
+ LockedPage, CascadeLockedPage, LockedNoPage,
+ EditConflict, PageDeletedConflict, PageCreatedConflict,
ServerError, FatalServerError, Server504Error,
CaptchaError, SpamfilterError, CircularRedirect,
WikiBaseError, CoordinateGlobeUnknownException,
@@ -69,7 +71,10 @@
'Error', 'InvalidTitle', 'BadTitle', 'NoPage',
'SectionError',
'NoSuchSite', 'NoUsername', 'UserBlocked',
'PageRelatedError', 'IsRedirectPage',
'IsNotRedirectPage',
- 'PageNotSaved', 'UploadWarning', 'LockedPage',
'EditConflict',
+ 'PageSaveRelatedError', 'PageNotSaved',
'OtherPageSaveError',
+ 'LockedPage', 'CascadeLockedPage', 'LockedNoPage',
+ 'EditConflict', 'PageDeletedConflict',
'PageCreatedConflict',
+ 'UploadWarning',
'ServerError', 'FatalServerError', 'Server504Error',
'CaptchaError', 'SpamfilterError',
'CircularRedirect',
'WikiBaseError', 'CoordinateGlobeUnknownException',
diff --git a/pywikibot/exceptions.py b/pywikibot/exceptions.py
index 35f7d2d..661fcf5 100644
--- a/pywikibot/exceptions.py
+++ b/pywikibot/exceptions.py
@@ -1,6 +1,43 @@
# -*- coding: utf-8 -*-
"""
Exception classes used throughout the framework.
+
+Error: Base class, all exceptions should the subclass of this class.
+ - NoUsername: Username is not in user-config.py
+ - UserBlockedY: our username or IP has been blocked
+ - AutoblockUser: requested action on a virtual autoblock user not valid
+ - UserActionRefuse
+ - NoSuchSite: Site does not exist
+ - BadTitle: Server responded with BadTitle
+ - InvalidTitle: Invalid page title
+ - PageNotFound: Page not found in list
+ - CaptchaError: Captcha is asked and config.solve_captcha == False
+ - Server504Error: Server timed out with HTTP 504 code
+
+PageRelatedError: any exception which is caused by an operation on a Page.
+ - NoPage: Page does not exist
+ - IsRedirectPage: Page is a redirect page
+ - IsNotRedirectPage: Page is not a redirect page
+ - CircularRedirect: Page is a circular redirect
+ - SectionError: The section specified by # does not exist
+ - LockedPage: Page is locked
+ - LockedNoPage: Title is locked against creation
+ - CascadeLockedPage: Page is locked due to cascading protection
+
+PageSaveRelatedError: page exceptions within the save operation on a Page.
+ (alias: PageNotSaved)
+ - SpamfilterError: MediaWiki spam filter detected a blacklisted URL
+ - OtherPageSaveError: misc. other save related exception.
+ - EditConflict: Edit conflict while uploading the page
+ - PageDeletedConflict: Page was deleted since being retrieved
+ - PageCreatedConflict: Page was created by another user
+
+ServerError: a problem with the server.
+ - FatalServerError: A fatal/non-recoverable server error
+
+WikiBaseError: any issue specific to Wikibase.
+ - CoordinateGlobeUnknownException: globe is not implemented yet.
+
"""
#
# (C) Pywikibot team, 2008
@@ -29,25 +66,72 @@
class PageRelatedError(Error):
- """Abstract Exception, used when the Exception concerns a particular
- Page, and when a generic message can be written once for all"""
+ """
+ Abstract Exception, used when the exception concerns a particular Page.
+ """
+
# Preformated UNICODE message where the page title will be inserted
# Override this in subclasses.
# u"Oh noes! Page %s is too funky, we should not delete it ;("
message = None
- def __init__(self, page):
+ def __init__(self, page, message=None):
"""
- @param page
+ Constructor.
+
+ @param page: Page that caused the exception
@type page: Page object
"""
+ if message:
+ self.message = message
+
if self.message is None:
raise Error("PageRelatedError is abstract. Can't instantiate
it!")
- super(PageRelatedError, self).__init__(self.message % page)
- self._page = page
+
+ self.page = page
+ self.title = page.title(asLink=True)
+ self.site = page.site
+
+ if '%(' in self.message and ')s' in self.message:
+ super(PageRelatedError, self).__init__(self.message % self.__dict__)
+ else:
+ super(PageRelatedError, self).__init__(self.message % page)
def getPage(self):
return self._page
+
+
+class PageSaveRelatedError(PageRelatedError):
+
+ """Saving the page has failed"""
+ message = u"Page %s was not saved."
+
+ # This property maintains backwards compatibility with
+ # the old PageNotSaved which inherited from Error
+ # (not PageRelatedError) and exposed the normal 'args'
+ # which could be printed
+ @property
+ def args(self):
+ return unicode(self)
+
+
+class OtherPageSaveError(PageSaveRelatedError):
+
+ """Saving the page has failed due to uncatchable
error."""
+ message = "Edit to page %(title)s failed:\n%(reason)s"
+
+ def __init__(self, page, reason):
+ """ Constructor.
+
+ @param reason: Details of the problem
+ @type reason: Exception or basestring
+ """
+ self.reason = reason
+ super(OtherPageSaveError, self).__init__(page)
+
+ @property
+ def args(self):
+ return unicode(self.reason)
class NoUsername(Error):
@@ -95,10 +179,22 @@
"""Invalid page title"""
-class LockedPage(PageRelatedError):
+class LockedPage(PageSaveRelatedError):
"""Page is locked"""
message = u"Page %s is locked."
+
+
+class LockedNoPage(LockedPage):
+
+ """Title is locked against creation"""
+ message = u"Page %s does not exist and is locked preventing creation."
+
+
+class CascadeLockedPage(LockedPage):
+
+ """Page is locked due to cascading protection"""
+ message = u"Page %s is locked due to cascading protection."
class SectionError(Error):
@@ -106,26 +202,38 @@
"""The section specified by # does not exist"""
-class PageNotSaved(Error):
-
- """Saving the page has failed"""
+PageNotSaved = PageSaveRelatedError
-class EditConflict(PageNotSaved):
+class EditConflict(PageSaveRelatedError):
"""There has been an edit conflict while uploading the
page"""
+ message = u"Page %s could not be saved due to an edit conflict"
-class SpamfilterError(PageNotSaved):
+class PageDeletedConflict(EditConflict):
+
+ """Page was deleted since being retrieved"""
+ message = u"Page %s has been deleted since last retrieved."
+
+
+class PageCreatedConflict(EditConflict):
+
+ """Page was created by another user"""
+ message = u"Page %s has been created since last retrieved."
+
+
+class SpamfilterError(PageSaveRelatedError):
"""Saving the page has failed because the MediaWiki spam filter
detected a
blacklisted URL.
"""
- def __init__(self, arg):
- super(SpamfilterError, self).__init__(
- u'MediaWiki spam filter has been triggered')
- self.url = arg
- self.args = arg,
+
+ message = "Edit to page %(title)s rejected by spam filter due to
content:\n%(url)s"
+
+ def __init__(self, page, url):
+ self.url = url
+ super(SpamfilterError, self).__init__(page)
class ServerError(Error):
diff --git a/pywikibot/page.py b/pywikibot/page.py
index d30e649..e81aae5 100644
--- a/pywikibot/page.py
+++ b/pywikibot/page.py
@@ -960,9 +960,8 @@
else:
watchval = "unwatch"
if not force and not self.botMayEdit():
- raise pywikibot.PageNotSaved(
- "Page %s not saved; editing restricted by {{bots}} template"
- % self.title(asLink=True))
+ raise pywikibot.OtherPageSaveError(
+ self, "Editing restricted by {{bots}} template")
if botflag is None:
botflag = ("bot" in self.site.userinfo["rights"])
if async:
@@ -986,20 +985,17 @@
watch=watchval, bot=botflag, **kwargs)
if not done:
pywikibot.warning(u"Page %s not saved" % link)
- raise pywikibot.PageNotSaved(link)
+ raise pywikibot.PageNotSaved(self)
else:
pywikibot.output(u"Page %s saved" % link)
- except pywikibot.LockedPage as err:
- # re-raise the LockedPage exception so that calling program
- # can re-try if appropriate
- if not callback and not async:
- raise
# TODO: other "expected" error types to catch?
except pywikibot.Error as err:
pywikibot.log(u"Error saving page %s (%s)\n" % (link, err),
exc_info=True)
if not callback and not async:
- raise pywikibot.PageNotSaved("%s: %s" % (link, err))
+ if isinstance(err, pywikibot.PageSaveRelatedError):
+ raise err
+ raise pywikibot.OtherPageSaveError(self, err)
if callback:
callback(self, err)
diff --git a/pywikibot/site.py b/pywikibot/site.py
index 07a280c..d47fda3 100644
--- a/pywikibot/site.py
+++ b/pywikibot/site.py
@@ -33,9 +33,14 @@
from pywikibot.throttle import Throttle
from pywikibot.data import api
from pywikibot.exceptions import (
- EditConflict,
Error,
+ PageSaveRelatedError,
+ EditConflict,
+ PageCreatedConflict,
+ PageDeletedConflict,
LockedPage,
+ CascadeLockedPage,
+ LockedNoPage,
NoPage,
NoSuchSite,
NoUsername,
@@ -3373,23 +3378,26 @@
rngen.request["grnredirect"] = ""
return rngen
- # catalog of editpage error codes, for use in generating messages
+ # Catalog of editpage error codes, for use in generating messages.
+ # The block at the bottom are page related errors.
_ep_errors = {
"noapiwrite": "API editing not enabled on %(site)s wiki",
"writeapidenied": "User %(user)s is not authorized to edit on
%(site)s wiki",
- "protectedtitle": "Title %(title)s is protected against creation
on %(site)s",
"cantcreate": "User %(user)s not authorized to create new pages on
%(site)s wiki",
"cantcreate-anon": """Bot is not logged in, and anon
users are not authorized to create new pages on %(site)s wiki""",
- "articleexists": "Page %(title)s already exists on %(site)s
wiki",
"noimageredirect-anon": """Bot is not logged in, and
anon users are not authorized to create image redirects on %(site)s
wiki""",
"noimageredirect": "User %(user)s not authorized to create image
redirects on %(site)s wiki",
- "spamdetected": "Edit to page %(title)s rejected by spam filter
due to content:\n",
"filtered": "%(info)s",
"contenttoobig": "%(info)s",
"noedit-anon": """Bot is not logged in, and anon users
are not authorized to edit on %(site)s wiki""",
"noedit": "User %(user)s not authorized to edit pages on %(site)s
wiki",
- "pagedeleted": "Page %(title)s has been deleted since last
retrieved from %(site)s wiki",
- "editconflict": "Page %(title)s not saved due to edit
conflict.",
+
+ "editconflict": EditConflict,
+ "articleexists": PageCreatedConflict,
+ "pagedeleted": PageDeletedConflict,
+ "protectedpage": LockedPage,
+ "protectedtitle": LockedNoPage,
+ "cascadeprotected": CascadeLockedPage,
}
@must_be(group='user')
@@ -3437,8 +3445,7 @@
# before the page is saved.
self.lock_page(page)
if lastrev is not None and page.latestRevision() != lastrev:
- raise EditConflict(
- "editpage: Edit conflict detected; saving aborted.")
+ raise EditConflict(page)
params = dict(action="edit",
title=page.title(withSection=False),
text=text, token=token, summary=summary)
@@ -3477,23 +3484,17 @@
u"editpage: received '%s' even though bot is logged
in"
% err.code,
_logger)
- errdata = {
- 'site': self,
- 'title': page.title(withSection=False),
- 'user': self.user(),
- 'info': err.info
- }
- if err.code == "spamdetected":
- raise SpamfilterError(
- self._ep_errors[err.code] % errdata
- + err.info[err.info.index("fragment: ") + 9:])
-
- if err.code == "editconflict":
- raise EditConflict(self._ep_errors[err.code] % errdata)
- if err.code in ("protectedpage",
"cascadeprotected"):
- raise LockedPage(errdata['title'])
if err.code in self._ep_errors:
- raise Error(self._ep_errors[err.code] % errdata)
+ if issubclass(self._ep_errors[err.code], PageSaveRelatedError):
+ raise self._ep_errors[err.code](page)
+ else:
+ errdata = {
+ 'site': self,
+ 'title': page.title(withSection=False),
+ 'user': self.user(),
+ 'info': err.info
+ }
+ raise Error(self._ep_errors[err.code] % errdata)
pywikibot.debug(
u"editpage: Unexpected error code '%s' received."
% err.code,
@@ -3537,6 +3538,8 @@
u"page not saved"
% captcha)
return False
+ elif 'spamblacklist' in result['edit']:
+ raise SpamfilterError(page,
result['edit']['spamblacklist'])
else:
self.unlock_page(page)
pywikibot.error(u"editpage: unknown failure reason %s"
@@ -3600,8 +3603,9 @@
raise Error("Cannot move page %s to its own title."
% oldtitle)
if not page.exists():
- raise NoPage("Cannot move page %s because it does not exist on
%s."
- % (oldtitle, self))
+ raise NoPage(page,
+ "Cannot move page %(page)s because it "
+ "does not exist on %(site)s.")
token = self.tokens['move']
self.lock_page(page)
req = api.Request(site=self, action="move", to=newtitle,
diff --git a/scripts/add_text.py b/scripts/add_text.py
index b3cd488..1d104e1 100644
--- a/scripts/add_text.py
+++ b/scripts/add_text.py
@@ -260,13 +260,13 @@
u'Cannot change %s because of blacklist entry %s'
% (page.title(), e.url))
return (False, False, always)
- except pywikibot.PageNotSaved as error:
- pywikibot.output(u'Error putting page: %s' % error.args)
- return (False, False, always)
except pywikibot.LockedPage:
pywikibot.output(u'Skipping %s (locked page)'
% page.title())
return (False, False, always)
+ except pywikibot.PageNotSaved as error:
+ pywikibot.output(u'Error putting page: %s' % error.args)
+ return (False, False, always)
else:
# Break only if the errors are one after the other...
errorCount = 0
diff --git a/scripts/blockpageschecker.py b/scripts/blockpageschecker.py
index 0417458..70c5fdf 100755
--- a/scripts/blockpageschecker.py
+++ b/scripts/blockpageschecker.py
@@ -474,14 +474,14 @@
u'blacklist entry %s'
% (page.title(), e.url))
break
- except pywikibot.PageNotSaved as error:
- pywikibot.output(u'Error putting page: %s'
- % (error.args,))
- break
except pywikibot.LockedPage:
pywikibot.output(u'The page is still protected. '
u'Skipping...')
break
+ except pywikibot.PageNotSaved as error:
+ pywikibot.output(u'Error putting page: %s'
+ % (error.args,))
+ break
else:
# Break only if the errors are one after the other
errorCount = 0
diff --git a/scripts/reflinks.py b/scripts/reflinks.py
index 2d35316..b22636a 100644
--- a/scripts/reflinks.py
+++ b/scripts/reflinks.py
@@ -744,11 +744,11 @@
pywikibot.output(
u'Cannot change %s because of blacklist entry %s'
% (page.title(), e.url))
- except pywikibot.PageNotSaved as error:
- pywikibot.error(u'putting page: %s' % (error.args,))
except pywikibot.LockedPage:
pywikibot.output(u'Skipping %s (locked page)'
% page.title())
+ except pywikibot.PageNotSaved as error:
+ pywikibot.error(u'putting page: %s' % (error.args,))
except pywikibot.ServerError as e:
pywikibot.output(u'Server Error : %s' % e)
diff --git a/scripts/replace.py b/scripts/replace.py
index a8e6b46..9d92a1e 100755
--- a/scripts/replace.py
+++ b/scripts/replace.py
@@ -412,12 +412,12 @@
pywikibot.output(
u'Cannot change %s because of blacklist entry %s'
% (page.title(), e.url))
- except pywikibot.PageNotSaved as error:
- pywikibot.output(u'Error putting page: %s'
- % (error.args,))
except pywikibot.LockedPage:
pywikibot.output(u'Skipping %s (locked page)'
% (page.title(),))
+ except pywikibot.PageNotSaved as error:
+ pywikibot.output(u'Error putting page: %s'
+ % (error.args,))
def prepareRegexForMySQL(pattern):
diff --git a/tests/edit_failure_tests.py b/tests/edit_failure_tests.py
new file mode 100644
index 0000000..50010b8
--- /dev/null
+++ b/tests/edit_failure_tests.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for edit failures.
+
+These tests should never write to the wiki,
+unless something has broken badly.
+"""
+#
+# (C) Pywikibot team, 2014
+#
+# Distributed under the terms of the MIT license.
+#
+__version__ = '$Id$'
+
+import pywikibot
+from pywikibot import (
+ LockedPage,
+ SpamfilterError,
+ OtherPageSaveError,
+)
+from tests.utils import SiteTestCase, unittest
+
+
+class TestSaveFailure(SiteTestCase):
+ """Test cases for edits which should fail to save."""
+
+ write = True
+
+ def setUp(self):
+ super(TestSaveFailure, self).setUp()
+ self.site = pywikibot.Site('test', 'wikipedia')
+
+ def test_protected(self):
+ """Test that protected titles raise the appropriate
exception."""
+ if self.site._username[1]:
+ raise unittest.SkipTest('Testing failure of edit protected with a sysop
account')
+ page = pywikibot.Page(self.site, 'Wikipedia:Create a new page')
+ self.assertRaises(LockedPage, page.save)
+
+ def test_spam(self):
+ """Test that spam in content raise the appropriate
exception."""
+ page = pywikibot.Page(self.site, 'Wikipedia:Sandbox')
+ page.text = 'http://badsite.com'
+ self.assertRaisesRegexp(SpamfilterError, 'badsite.com', page.save)
+
+ def test_nobots(self):
+ """Test that {{nobots}} raise the appropriate
exception."""
+ page = pywikibot.Page(self.site, 'User:John Vandenberg/nobots')
+ self.assertRaisesRegexp(OtherPageSaveError, 'nobots', page.save)
+
+if __name__ == '__main__':
+ try:
+ unittest.main()
+ except SystemExit:
+ pass
--
To view, visit
https://gerrit.wikimedia.org/r/141398
To unsubscribe, visit
https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: merged
Gerrit-Change-Id: Id8b8c8055100329e7844b5db6500f71bbb1673f7
Gerrit-PatchSet: 11
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Owner: John Vandenberg <jayvdb(a)gmail.com>
Gerrit-Reviewer: John Vandenberg <jayvdb(a)gmail.com>
Gerrit-Reviewer: Ladsgroup <ladsgroup(a)gmail.com>
Gerrit-Reviewer: Merlijn van Deen <valhallasw(a)arctus.nl>
Gerrit-Reviewer: Mpaa <mpaa.wiki(a)gmail.com>
Gerrit-Reviewer: XZise <CommodoreFabianus(a)gmx.de>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: jenkins-bot <>