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
pywikibot-commits@lists.wikimedia.org