jenkins-bot has submitted this change and it was merged.
Change subject: Introduce InterwikiRedirectPage ......................................................................
Introduce InterwikiRedirectPage
Interwiki links in redirects will raise InterwikiRedirectPage instead of CircularRedirect exception.
Interwiki redirects are considered an invalid page by Pywikibot due to limited support for this kind of redirects on Mediawiki. The API can't follow redirects if the redirect is an interwiki link.
Also fixed: - uniform importing of exceptions in site.py - pep257 in pywikibot.exceptions
Bug: 73184 Change-Id: Ia0d4dadf713fb97572c5d482485858331bda5ea8 --- M pywikibot/exceptions.py M pywikibot/site.py 2 files changed, 87 insertions(+), 22 deletions(-)
Approvals: John Vandenberg: Looks good to me, approved jenkins-bot: Verified
diff --git a/pywikibot/exceptions.py b/pywikibot/exceptions.py index 298ef6c..8300ecd 100644 --- a/pywikibot/exceptions.py +++ b/pywikibot/exceptions.py @@ -22,6 +22,7 @@ - IsRedirectPage: Page is a redirect page - IsNotRedirectPage: Page is not a redirect page - CircularRedirect: Page is a circular redirect + - InterwikiRedirectPage: Page is a redirect to another site. - SectionError: The section specified by # does not exist
PageSaveRelatedError: page exceptions within the save operation on a Page @@ -62,9 +63,11 @@
# NOTE: UnicodeMixin must be the first object Error class is derived from. def __init__(self, arg): + """Constructor.""" self.unicode = arg
def __unicode__(self): + """Return a unicode string representation.""" return self.unicode
@@ -77,7 +80,7 @@ Page, and when a generic message can be written once for all. """
- # Preformated UNICODE message where the page title will be inserted + # Preformatted 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 @@ -105,6 +108,7 @@ super(PageRelatedError, self).__init__(self.message % page)
def getPage(self): + """Return the page related to the exception.""" return self.page
@@ -120,6 +124,7 @@ # which could be printed @property def args(self): + """Expose args.""" return unicode(self)
@@ -140,6 +145,7 @@
@property def args(self): + """Expose args.""" return unicode(self.reason)
@@ -215,6 +221,30 @@ """
message = u"Page %s is a circular redirect." + + +class InterwikiRedirectPage(PageRelatedError): + + """ + Page is a redirect to another site. + + This is considered invalid in Pywikibot. See Bug 73184. + + """ + + message = (u"Page redirects to a page on another Site.\n" + u"Page: %(page)s\n" + u"Target page: %(target_page)s on %(target_site)s.") + + def __init__(self, page, target_page): + """ Constructor. + + @param target_page: Target page of the redirect. + @type reason: Page + """ + self.target_page = target_page + self.target_site = target_page.site + super(InterwikiRedirectPage, self).__init__(page)
class InvalidTitle(Error): # noqa @@ -313,6 +343,7 @@ message = "Edit to page %(title)s rejected by spam filter due to content:\n%(url)s"
def __init__(self, page, url): + """Constructor.""" self.url = url super(SpamfilterError, self).__init__(page)
diff --git a/pywikibot/site.py b/pywikibot/site.py index ba1b00c..8bcb658 100644 --- a/pywikibot/site.py +++ b/pywikibot/site.py @@ -42,6 +42,9 @@ PageCreatedConflict, PageDeletedConflict, ArticleExistsConflict, + IsNotRedirectPage, + CircularRedirect, + InterwikiRedirectPage, LockedPage, CascadeLockedPage, LockedNoPage, @@ -52,6 +55,7 @@ SpamfilterError, NoCreateError, UserBlocked, + EntityTypeUnknownException, )
from pywikibot.echo import Notification @@ -447,7 +451,7 @@ and oldcode == pywikibot.config.mylang: pywikibot.config.mylang = self.__code else: - raise UnknownSite("Language %s does not exist in family %s" + raise UnknownSite("Language '%s' does not exist in family %s" % (self.__code, self.__family.name))
self.nocapitalize = self.code in self.family.nocapitalize @@ -2341,9 +2345,10 @@ def getredirtarget(self, page): """Return Page object for the redirect target of page.""" if not self.page_isredirect(page): - raise pywikibot.IsNotRedirectPage(page) + raise IsNotRedirectPage(page) if hasattr(page, '_redirtarget'): return page._redirtarget + title = page.title(withSection=False) query = api.Request(site=self, action="query", prop="info", inprop="protection|talkid|subjectid", @@ -2354,35 +2359,64 @@ raise RuntimeError( "getredirtarget: No 'redirects' found for page %s." % title.encode(self.encoding())) + redirmap = dict((item['from'], {'title': item['to'], 'section': u'#' + item['tofragment'] if 'tofragment' in item and item['tofragment'] else ''}) for item in result['query']['redirects']) - if 'normalized' in result['query']: - for item in result['query']['normalized']: - if item['from'] == title: - title = item['to'] - break + + # Normalize title + for item in result['query'].get('normalized', []): + if item['from'] == title: + title = item['to'] + break + if title not in redirmap: raise RuntimeError( "getredirtarget: 'redirects' contains no key for page %s." % title.encode(self.encoding())) target_title = u'%(title)s%(section)s' % redirmap[title] - if target_title == title or "pages" not in result['query']: - # no "pages" element indicates a circular redirect - raise pywikibot.CircularRedirect(page) + + if self.sametitle(title, target_title): + raise CircularRedirect(page) + + if "pages" not in result['query']: + # No "pages" element might indicate a circular redirect + # Check that a "to" link is also a "from" link in redirmap + for _from, _to in redirmap.items(): + if _to['title'] in redirmap: + raise CircularRedirect(page) + else: + target = pywikibot.Page(source=page.site, title=target_title) + + # Check if target is on another site. + if target.site != page.site: + raise InterwikiRedirectPage(page, target) + else: + # Redirect to Special: & Media: pages, which do not work + # like redirects, but are rendered like a redirect. + page._redirtarget = target + return page._redirtarget + pagedata = list(result['query']['pages'].values())[0] - # there should be only one value in 'pages', and it is the target + # There should be only one value in 'pages' (the ultimate + # target, also in case of double redirects). if self.sametitle(pagedata['title'], target_title): + # target_title is the ultimate target target = pywikibot.Page(self, pagedata['title'], pagedata['ns']) api.update_page(target, pagedata, ['info']) page._redirtarget = target else: - # double redirect; target is an intermediate redirect + # Target is an intermediate redirect -> double redirect. + # Do not bypass double-redirects and return the ultimate target; + # it would be impossible to detect and fix double-redirects. + # This handles also redirects to sections, as sametitle() + # does not ignore sections. target = pywikibot.Page(self, target_title) page._redirtarget = target + return page._redirtarget
def preloadpages(self, pagelist, groupsize=50, templates=False, @@ -3272,12 +3306,12 @@ if starttime and endtime: if reverse: if starttime > endtime: - raise pywikibot.Error( + raise Error( "blocks: " "starttime must be before endtime with reverse=True") else: if endtime > starttime: - raise pywikibot.Error( + raise Error( "blocks: " "endtime must be before starttime with reverse=False") bkgen = self._generator(api.ListGenerator, type_arg="blocks", @@ -4002,7 +4036,7 @@
""" if len(page._revisions) < 2: - raise pywikibot.Error( + raise Error( u"Rollback of %s aborted; load revision history first." % page.title(asLink=True)) last_rev = page._revisions[page.latestRevision()] @@ -4012,7 +4046,7 @@ if rev.user != last_user: break else: - raise pywikibot.Error( + raise Error( u"Rollback of %s aborted; only one user in revision history." % page.title(asLink=True)) token = self.tokens["rollback"] @@ -4428,7 +4462,7 @@
# check for required user right if "upload" not in self.userinfo["rights"]: - raise pywikibot.Error( + raise Error( "User '%s' does not have upload rights on site %s." % (self.user(), self)) # check for required parameters @@ -4516,7 +4550,7 @@ else: # upload by URL if "upload_by_url" not in self.userinfo["rights"]: - raise pywikibot.Error( + raise Error( "User '%s' is not authorized to upload by URL on site %s." % (self.user(), self)) req = api.Request(site=self, action="upload", token=token, @@ -4908,7 +4942,7 @@ if isinstance(self._item_namespace, Namespace): return self._item_namespace else: - raise pywikibot.exceptions.EntityTypeUnknownException( + raise EntityTypeUnknownException( '%r does not support entity type "item"' % self)
@@ -4926,7 +4960,7 @@ if isinstance(self._property_namespace, Namespace): return self._property_namespace else: - raise pywikibot.exceptions.EntityTypeUnknownException( + raise EntityTypeUnknownException( '%r does not support entity type "property"' % self)
@@ -5130,7 +5164,7 @@ raise NotImplementedError if not claim.snak: # We need to already have the snak value - raise pywikibot.NoPage(claim) + raise NoPage(claim) params = dict(action='wbsetclaimvalue', claim=claim.snak, snaktype=snaktype,
pywikibot-commits@lists.wikimedia.org