John Vandenberg has uploaded a new change for review.

  https://gerrit.wikimedia.org/r/237977

Change subject: Expand login and rights exception tree
......................................................................

Expand login and rights exception tree

Allow APIError, with extra information, to be used as subclasses
for core exceptions like NoUsername, so that uncaught errors
include API supplied information like `servedby`.

Bug: T109173
Change-Id: I7f1119f2ccea48221ca400958683fd5de13ee34d
---
M pywikibot/data/api.py
M pywikibot/exceptions.py
M pywikibot/login.py
M pywikibot/site.py
M scripts/category.py
M scripts/interwiki.py
M scripts/redirect.py
M tests/exceptions_tests.py
8 files changed, 394 insertions(+), 81 deletions(-)


  git pull ssh://gerrit.wikimedia.org:29418/pywikibot/core 
refs/changes/77/237977/1

diff --git a/pywikibot/data/api.py b/pywikibot/data/api.py
index e574227..a8cc216 100644
--- a/pywikibot/data/api.py
+++ b/pywikibot/data/api.py
@@ -28,12 +28,21 @@
 from warnings import warn
 
 import pywikibot
+
 from pywikibot import config, login
-from pywikibot.tools import MediaWikiVersion, deprecated, itergroup, ip, PY2
-from pywikibot.exceptions import (
-    Server504Error, Server414Error, FatalServerError, NoUsername, Error
-)
+
 from pywikibot.comms import http
+from pywikibot.exceptions import (
+    Server504Error, Server414Error, FatalServerError,
+    SiteRelatedError,
+    LoginError,
+    OAuthAuthenticationError,
+    Error,
+)
+from pywikibot.tools import (
+    MediaWikiVersion, itergroup, ip, PY2,
+    deprecated, issue_deprecation_warning,
+)
 
 if not PY2:
     # Subclassing necessary to fix a possible bug of the email package
@@ -87,23 +96,31 @@
 lagpattern = re.compile(r"Waiting for [\d.]+: (?P<lag>\d+) seconds? lagged")
 
 
-class APIError(Error):
+class APIError(SiteRelatedError):
 
     """The wiki site returned an error message."""
 
-    def __init__(self, code, info, **kwargs):
+    def __init__(self, code, info, site=None, **kwargs):
         """Save error dict returned by MW API."""
         self.code = code
         self.info = info
         self.other = kwargs
-        self.unicode = unicode(self.__str__())
+        self.unicode = unicode(self)
+        message = self.unicode
+
+        if site:
+            super(APIError, self).__init__(site, message)
+        else:
+            issue_deprecation_warning(
+                'APIError without a site', instead=None, depth=2)
+            super(APIError, self).__init__(message=message)
 
     def __repr__(self):
         """Return internal representation."""
         return '{name}("{code}", "{info}", {other})'.format(
             name=self.__class__.__name__, **self.__dict__)
 
-    def __str__(self):
+    def __unicode__(self):
         """Return a string representation."""
         if self.other:
             return '{0}: {1} [{2}]'.format(
@@ -120,7 +137,7 @@
 
     """Upload failed with a warning message (passed as the argument)."""
 
-    def __init__(self, code, message, file_key=None, offset=0):
+    def __init__(self, code, message, file_key=None, offset=0, site=None):
         """
         Create a new UploadWarning instance.
 
@@ -131,7 +148,7 @@
             there is no offset.
         @type offset: int or bool
         """
-        super(UploadWarning, self).__init__(code, message)
+        super(UploadWarning, self).__init__(code, message, site)
         self.file_key = file_key
         self.offset = offset
 
@@ -150,6 +167,16 @@
         self.mediawiki_exception_class_name = mediawiki_exception_class_name
         code = 'internal_api_error_' + mediawiki_exception_class_name
         super(APIMWException, self).__init__(code, info, **kwargs)
+
+
+class APILoginError(APIError, LoginError):
+
+    """Login error during an API request."""
+
+
+class APIOAuthInvalidAuthorisation(APIError, OAuthAuthenticationError):
+
+    """OAuth failure during an API request."""
 
 
 class ParamInfo(Container):
@@ -2150,10 +2177,12 @@
                             self.site.user(),
                             ', '.join('{0}: {1}'.format(*e)
                                       for e in user_tokens.items())))
+
             if 'mwoauth-invalid-authorization' in code:
-                raise NoUsername('Failed OAuth authentication for %s: %s'
-                                 % (self.site, info))
-            # raise error
+                raise APIOAuthInvalidAuthorisation(site=self.site, **error)
+            elif code == 'readapidenied':
+                raise APILoginError(site=self.site, **error)
+
             try:
                 # Due to bug T66958, Page's repr may return non ASCII bytes
                 # Get as bytes in PY2 and decode with the console encoding as
diff --git a/pywikibot/exceptions.py b/pywikibot/exceptions.py
index a8a8beb..221f191 100644
--- a/pywikibot/exceptions.py
+++ b/pywikibot/exceptions.py
@@ -3,10 +3,7 @@
 Exception and warning classes used throughout the framework.
 
 Error: Base class, all exceptions should the subclass of this class.
-  - NoUsername: Username is not in user-config.py, or it is invalid.
-  - UserBlocked: Username or IP has been blocked
   - AutoblockUser: requested action on a virtual autoblock user not valid
-  - UserRightsError: insufficient rights for requested action
   - BadTitle: Server responded with BadTitle
   - InvalidTitle: Invalid page title
   - CaptchaError: Captcha is asked and config.solve_captcha == False
@@ -14,6 +11,13 @@
   - PageNotFound: Page not found (deprecated)
   - i18n.TranslationError: i18n/l10n message not available
   - UnknownExtension: Extension is not defined for this site
+
+---NotLoggedIn: A login was not possible
+  - NoUsername: Username is not in user-config.py, or it is invalid.
+      - LoginFailed: Login was not successful
+  - OAuthLoginFailure: OAuth login failure
+      - login.OAuthImpossible: OAuth dependencies failed
+      - data.api.OAuthInvalidAuthorisation: OAuth authorisation failed
 
 SiteDefinitionError: Site loading problem
   - UnknownSite: Site does not exist in Family
@@ -66,7 +70,7 @@
   - FamilyMaintenanceWarning: missing information in family definition
 """
 #
-# (C) Pywikibot team, 2008
+# (C) Pywikibot team, 2008-2015
 #
 # Distributed under the terms of the MIT license.
 #
@@ -76,9 +80,14 @@
 
 import sys
 
-from pywikibot.tools import UnicodeMixin, _NotImplementedWarning
+from pywikibot.tools import (
+    PY2,
+    UnicodeMixin,
+    _NotImplementedWarning,
+    issue_deprecation_warning,
+)
 
-if sys.version_info[0] > 2:
+if not PY2:
     unicode = str
 
 
@@ -111,13 +120,57 @@
     def __init__(self, arg):
         """Constructor."""
         self.unicode = arg
+        super(Error, self).__init__(arg)
 
     def __unicode__(self):
         """Return a unicode string representation."""
         return self.unicode
 
 
-class PageRelatedError(Error):
+class SiteRelatedError(Error):
+
+    """
+    Abstract Exception, used when the exception concerns a particular Site.
+
+    This class should be used when the Exception concerns a particular
+    Site, and when a generic message can be written once for all.
+    """
+
+    def __init__(self, site=None, message=None):
+        """
+        Constructor.
+
+        @param page: Site that caused the exception
+        @type page: Site object
+        """
+        if message:
+            try:
+                self.message = message
+            except AttributeError:
+                pass
+
+        if not site:
+            issue_deprecation_warning(
+                'SiteRelatedError(site=None)', instead=None, depth=2)
+            self.site = self.family = self.site_code = 'unknown'
+        else:
+            self.site = site
+            self.family = self.site.family
+            self.site_code = self.site.code
+
+        if hasattr(self, 'message'):
+            if '%(' in self.message and ')s' in self.message:
+                super(SiteRelatedError, self).__init__(
+                    self.message % self.__dict__)
+            elif '%s' in self.message:
+                super(SiteRelatedError, self).__init__(self.message % site)
+            else:
+                super(SiteRelatedError, self).__init__(self.message)
+        else:
+            super(SiteRelatedError, self).__init__()
+
+
+class PageRelatedError(SiteRelatedError):
 
     """
     Abstract Exception, used when the exception concerns a particular Page.
@@ -146,12 +199,11 @@
 
         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)
+        if '%s' in self.message:
+            self.message = self.message % page
+
+        super(PageRelatedError, self).__init__(page.site)
 
     def getPage(self):
         """Return the page related to the exception."""
@@ -195,11 +247,68 @@
         return unicode(self.reason)
 
 
-class NoUsername(Error):
+class UserRightsError(Error):
+
+    """Insufficient user rights to perform an action."""
+
+    pass
+
+
+class LoginError(Error):
+
+    """Unable to login."""
+
+
+class NoUsername(LoginError):
+
+    """Backwards compatible exception."""
+
+
+class LoginNotPossible(NoUsername, LoginError):
+
+    """Login is not possible."""
+
+
+class UsernameNotSpecified(LoginNotPossible, SiteRelatedError):
 
     """Username is not in user-config.py."""
 
-    pass
+    message = """
+ERROR: Username for %(site)s is undefined.
+If you have an account for that site, please add a line to user-config.py:
+
+usernames['%(family)s']['%(site_code)s'] = 'myUsername'"""
+
+
+class _SysopnameNotSpecified(UsernameNotSpecified):
+
+    """Sysopname is not in user-config.py."""
+
+    message = """
+ERROR: Sysop username for %(site)s is undefined.
+If you have a sysop account for that site, please add a line to user-config.py:
+
+sysopnames['%(family)s']['%(site_code)s'] = 'myUsername'"""
+
+
+class InvalidUsername(LoginNotPossible):
+
+    """Username is not recognised."""
+
+
+class LoginFailed(NoUsername, LoginError):
+
+    """Login attempt failed."""
+
+
+class RightsElevationFailed(LoginFailed, UserRightsError):
+
+    """Attempt to increase user rights failed."""
+
+
+class OAuthAuthenticationError(LoginError):
+
+    """OAuth authentication error."""
 
 
 class NoPage(PageRelatedError):  # noqa
@@ -449,14 +558,14 @@
     pass
 
 
-class UserBlocked(Error):  # noqa
+class UserBlocked(UserRightsError):  # noqa
 
     """Your username or IP has been blocked"""
 
     pass
 
 
-class CaptchaError(Error):
+class CaptchaError(LoginError):
 
     """Captcha is asked and config.solve_captcha == False."""
 
@@ -471,13 +580,6 @@
     an action is requested on a virtual autoblock user that's not available
     for him (i.e. roughly everything except unblock).
     """
-
-    pass
-
-
-class UserRightsError(Error):
-
-    """Insufficient user rights to perform an action."""
 
     pass
 
diff --git a/pywikibot/login.py b/pywikibot/login.py
index ac37803..8e86cfd 100644
--- a/pywikibot/login.py
+++ b/pywikibot/login.py
@@ -27,10 +27,16 @@
 
 from pywikibot import config
 from pywikibot.tools import deprecated_args, normalize_username
-from pywikibot.exceptions import NoUsername
+from pywikibot.exceptions import (
+    LoginError,
+    RightsElevationFailed,
+    UsernameNotSpecified,
+    _SysopnameNotSpecified,
+    InvalidUsername,
+)
 
 
-class OAuthImpossible(ImportError):
+class OAuthImpossible(ImportError, LoginError):
 
     """OAuth authentication is not possible on your system."""
 
@@ -79,7 +85,7 @@
             The sysop username is loaded from config.sysopnames.
         @type sysop: bool
 
-        @raises NoUsername: No username is configured for the requested site.
+        @raises UsernameNotSpecified: No username for the requested site.
         """
         if site is not None:
             self.site = site
@@ -93,26 +99,15 @@
                 self.username = family_sysopnames.get(self.site.code, None)
                 self.username = self.username or family_sysopnames['*']
             except KeyError:
-                raise NoUsername(u"""\
-ERROR: Sysop username for %(fam_name)s:%(wiki_code)s is undefined.
-If you have a sysop account for that site, please add a line to user-config.py:
-
-sysopnames['%(fam_name)s']['%(wiki_code)s'] = 'myUsername'"""
-                                 % {'fam_name': self.site.family.name,
-                                    'wiki_code': self.site.code})
+                raise UsernameNotSpecified(self.site)
         else:
             try:
                 family_usernames = config.usernames[self.site.family.name]
                 self.username = family_usernames.get(self.site.code, None)
                 self.username = self.username or family_usernames['*']
             except:
-                raise NoUsername(u"""\
-ERROR: Username for %(fam_name)s:%(wiki_code)s is undefined.
-If you have an account for that site, please add a line to user-config.py:
+                raise _SysopnameNotSpecified(self.site)
 
-usernames['%(fam_name)s']['%(wiki_code)s'] = 'myUsername'"""
-                                 % {'fam_name': self.site.family.name,
-                                    'wiki_code': self.site.code})
         self.password = password
         if getattr(config, 'password_file', ''):
             self.readPassword()
@@ -121,12 +116,12 @@
         """
         Check that the username exists on the site.
 
-        @raises NoUsername: Username doesnt exist in user list.
+        @raises InvalidUsername: Username doesnt exist in user list.
         """
         try:
             data = self.site.allusers(start=self.username, total=1)
             user = next(iter(data))
-        except pywikibot.data.api.APIError as e:
+        except pywikibot.data.api.APILoginError as e:
             if e.code == 'readapidenied':
                 pywikibot.warning('Could not check user %s exists on %s'
                                   % (self.username, self.site))
@@ -136,8 +131,8 @@
 
         if user['name'] != self.username:
             # Report the same error as server error code NotExists
-            raise NoUsername('Username \'%s\' is invalid on %s'
-                             % (self.username, self.site))
+            raise InvalidUsername('Username \'%s\' is invalid on %s'
+                                  % (self.username, self.site))
 
     def botAllowed(self):
         """
@@ -257,7 +252,7 @@
         @param retry: infinitely retry if the API returns an unknown error
         @type retry: bool
 
-        @raises NoUsername: Username is not recognised by the site.
+        @raises : Username is not recognised by the site.
         """
         if not self.password:
             # First check that the username exists,
@@ -278,10 +273,10 @@
         except pywikibot.data.api.APIError as e:
             pywikibot.error(u"Login failed (%s)." % e.code)
             if e.code == 'NotExists':
-                raise NoUsername(u"Username '%s' does not exist on %s"
+                raise InvalidUsername(u"Username '%s' does not exist on %s"
                                  % (self.username, self.site))
             elif e.code == 'Illegal':
-                raise NoUsername(u"Username '%s' is invalid on %s"
+                raise InvalidUsername(u"Username '%s' is invalid on %s"
                                  % (self.username, self.site))
             # TODO: investigate other unhandled API codes (bug 73539)
             if retry:
@@ -332,7 +327,7 @@
             The sysop username is loaded from config.sysopnames.
         @type sysop: bool
 
-        @raises NoUsername: No username is configured for the requested site.
+        @raises : No username is configured for the requested site.
         @raise OAuthImpossible: mwoauth isn't installed
         """
         if isinstance(mwoauth, ImportError):
diff --git a/pywikibot/site.py b/pywikibot/site.py
index cbb092b..6dfa337 100644
--- a/pywikibot/site.py
+++ b/pywikibot/site.py
@@ -60,7 +60,8 @@
     UnknownSite,
     UnknownExtension,
     FamilyMaintenanceWarning,
-    NoUsername,
+    LoginError,
+    RightsElevationFailed,
     SpamfilterError,
     NoCreateError,
     UserBlocked,
@@ -1972,15 +1973,15 @@
             pass
         if self.is_oauth_token_available():
             if sysop:
-                raise NoUsername('No sysop is permitted with OAuth')
+                raise RightsElevationFailed('No sysop is permitted with OAuth')
             elif self.userinfo['name'] != self._username[sysop]:
-                raise NoUsername('Logged in on %(site)s via OAuth as 
%(wrong)s, '
+                raise LoginError('Logged in on %(site)s via OAuth as 
%(wrong)s, '
                                  'but expect as %(right)s'
                                  % {'site': self,
                                     'wrong': self.userinfo['name'],
                                     'right': self._username[sysop]})
             else:
-                raise NoUsername('Logging in on %s via OAuth failed' % self)
+                raise LoginFailure('Logging in on %s via OAuth failed' % self)
         loginMan = api.LoginManager(site=self, sysop=sysop,
                                     user=self._username[sysop])
         if loginMan.login(retry=True):
@@ -2879,7 +2880,7 @@
         sysop_protected = "edit" in rest and rest['edit'][0] == 'sysop'
         try:
             api.LoginManager(site=self, sysop=sysop_protected)
-        except NoUsername:
+        except LoginError:
             return False
         return True
 
@@ -4461,7 +4462,7 @@
         if "deletedhistory" not in self.userinfo['rights']:
             try:
                 self.login(True)
-            except NoUsername:
+            except RightsElevationFailed:
                 pass
             if "deletedhistory" not in self.userinfo['rights']:
                 raise Error(
@@ -4472,7 +4473,7 @@
             if "undelete" not in self.userinfo['rights']:
                 try:
                     self.login(True)
-                except NoUsername:
+                except RightsElevationFailed:
                     pass
                 if "undelete" not in self.userinfo['rights']:
                     raise Error(
diff --git a/scripts/category.py b/scripts/category.py
index 2bd8973..948b6fa 100755
--- a/scripts/category.py
+++ b/scripts/category.py
@@ -131,6 +131,7 @@
 from pywikibot.bot import (
     MultipleSitesBot, IntegerOption, StandardOption, ContextOption,
 )
+from pywikibot.exceptions import UsernameNotSpecified
 from pywikibot.tools import (
     deprecated_args, deprecated, ModuleDeprecationWrapper
 )
@@ -475,9 +476,9 @@
             repo = self.site.data_repository()
             if self.wikibase and repo.username() is None:
                 # The bot can't move categories nor update the Wikibase repo
-                raise pywikibot.NoUsername(u"The 'wikibase' option is turned 
on"
-                                           u" and %s has no registered 
username."
-                                           % repo)
+                raise UsernameNotSpecified(
+                    "The 'wikibase' option is turned on "
+                    'and %s has no registered username.', site=repo)
 
         template_vars = {'oldcat': self.oldcat.title(withNamespace=False)}
         if self.newcat:
diff --git a/scripts/interwiki.py b/scripts/interwiki.py
index beaab4c..7f0cfdf 100755
--- a/scripts/interwiki.py
+++ b/scripts/interwiki.py
@@ -360,6 +360,10 @@
 
 from pywikibot import config, i18n, pagegenerators, textlib, interwiki_graph, 
titletranslate
 from pywikibot.bot import ListOption, StandardOption
+from pywikibot.exceptions import (
+    LoginError,
+    UserRightsError,
+)
 from pywikibot.tools import first_upper
 
 if sys.version_info[0] > 2:
@@ -1706,7 +1710,7 @@
                                 updatedSites.append(site)
                         except SaveError:
                             notUpdatedSites.append(site)
-                        except pywikibot.NoUsername:
+                        except (LoginError, UserRightsError):
                             pass
                         except GiveUpOnPage:
                             break
diff --git a/scripts/redirect.py b/scripts/redirect.py
index 70ae9c7..4f76db2 100755
--- a/scripts/redirect.py
+++ b/scripts/redirect.py
@@ -83,8 +83,11 @@
 
 import sys
 import datetime
+
 import pywikibot
+
 from pywikibot import i18n, xmlreader, Bot
+from pywikibot.exceptions import UserRightsError
 
 if sys.version_info[0] > 2:
     basestring = (str, )
@@ -461,10 +464,10 @@
                                 % (targetPage, movedTarget, redir_page)):
                             try:
                                 redir_page.save(reason)
-                            except pywikibot.NoUsername:
-                                pywikibot.output(u"Page [[%s]] not saved; "
-                                                 u"sysop privileges required."
-                                                 % redir_page.title())
+                            except UserRightsError as e:
+                                pywikibot.output(
+                                    'Page [[%s]] not saved due to permissions'
+                                    % redir_page.title())
                             except pywikibot.LockedPage:
                                 pywikibot.output(u'%s is locked.'
                                                  % redir_page.title())
diff --git a/tests/exceptions_tests.py b/tests/exceptions_tests.py
index 252877b..ec9a239 100644
--- a/tests/exceptions_tests.py
+++ b/tests/exceptions_tests.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8  -*-
 """Tests for exceptions."""
 #
-# (C) Pywikibot team, 2014
+# (C) Pywikibot team, 2014-2015
 #
 # Distributed under the terms of the MIT license.
 #
@@ -11,21 +11,180 @@
 
 import pywikibot
 
-from tests.aspects import unittest, DeprecationTestCase
+from pywikibot.data.api import APIError
+from pywikibot.exceptions import (
+    Error,
+    SiteRelatedError,
+    PageRelatedError,
+    UsernameNotSpecified,
+    _SysopnameNotSpecified,
+)
+from pywikibot.tools import PY2
+
+from tests.aspects import (
+    unittest,
+    TestCase,
+    DefaultSiteTestCase,
+    DeprecationTestCase,
+)
+
+if not PY2:
+    unicode = str
 
 
-class TestDeprecatedExceptions(DeprecationTestCase):
+class TestError(TestCase):
+
+    """Test base Error class."""
+
+    net = False
+
+    def test_base(self):
+        """Test Error('foo')."""
+        exception = Error('foo')
+        self.assertEqual(str(exception), 'foo')
+        self.assertEqual(unicode(exception), 'foo')
+        self.assertEqual(exception.args, ('foo', ))
+        self.assertEqual(exception.message, 'foo')
+
+
+class TestSiteRelatedError(DefaultSiteTestCase):
+
+    """Test base Error class."""
+
+    dry = True
+
+    def test_site_related_error_base(self):
+        exception = SiteRelatedError(self.site, 'foo')
+        self.assertEqual(str(exception), 'foo')
+        self.assertEqual(unicode(exception), 'foo')
+
+        self.assertEqual(exception.args, ('foo', ))
+        self.assertEqual(exception.message, 'foo')
+
+        self.assertEqual(exception.site, self.site)
+        self.assertEqual(exception.family, self.site.family.name)
+        self.assertEqual(exception.site_code, self.site.code)
+
+    def test_site_related_error_percent(self):
+        exception = SiteRelatedError(self.site, 'foo %s bar')
+        expect = 'foo %s bar' % self.site
+        self.assertEqual(str(exception), expect)
+        self.assertEqual(unicode(exception), expect)
+
+        self.assertEqual(exception.args, (expect, ))
+        self.assertEqual(exception.message, 'foo %s bar')
+
+        self.assertEqual(exception.site, self.site)
+        self.assertEqual(exception.family, self.site.family.name)
+        self.assertEqual(exception.site_code, self.site.code)
+
+    def test_site_related_error_percent_dict(self):
+        exception = SiteRelatedError(self.site, 'foo %(site)s bar')
+        expect = 'foo %s bar' % self.site
+        self.assertEqual(str(exception), expect)
+        self.assertEqual(unicode(exception), expect)
+
+        self.assertEqual(exception.args, (expect, ))
+        self.assertEqual(exception.message, 'foo %(site)s bar')
+
+        self.assertEqual(exception.site, self.site)
+        self.assertEqual(exception.family, self.site.family.name)
+        self.assertEqual(exception.site_code, self.site.code)
+
+
+class TestPageRelatedError(DefaultSiteTestCase):
+
+    """Test base Error class."""
+
+    dry = True
+
+    def test_page_related_error_base(self):
+        mainpage = self.get_mainpage()
+        exception = PageRelatedError(mainpage, 'foo')
+
+        self.assertEqual(str(exception), 'foo')
+        self.assertEqual(unicode(exception), 'foo')
+
+        self.assertEqual(exception.args, ('foo', ))
+        self.assertEqual(exception.message, 'foo')
+
+        self.assertEqual(exception.site, self.site)
+
+    def test_page_related_error_percent(self):
+        mainpage = self.get_mainpage()
+        exception = PageRelatedError(mainpage, 'foo %s bar')
+        expect = 'foo %s bar' % mainpage
+
+        self.assertEqual(str(exception), expect)
+        self.assertEqual(unicode(exception), expect)
+
+        self.assertEqual(exception.args, (expect, ))
+        self.assertEqual(exception.message, expect)
+
+        self.assertEqual(exception.site, self.site)
+
+    def test_page_related_error_percent_dict_page(self):
+        mainpage = self.get_mainpage()
+        exception = PageRelatedError(mainpage, 'foo %(site)s %(page)s bar')
+        expect = 'foo %s %s bar' % (self.site, mainpage)
+
+        self.assertEqual(str(exception), expect)
+        self.assertEqual(unicode(exception), expect)
+
+        self.assertEqual(exception.args, (expect, ))
+        self.assertEqual(exception.message, 'foo %(site)s %(page)s bar')
+
+        self.assertEqual(exception.site, self.site)
+
+    def test_page_related_error_percent_dict_title(self):
+        mainpage = self.get_mainpage()
+        exception = PageRelatedError(mainpage, 'foo %(site)s %(title)s bar')
+        expect = 'foo %s %s bar' % (self.site, mainpage.title(asLink=True))
+
+        self.assertEqual(str(exception), expect)
+        self.assertEqual(unicode(exception), expect)
+
+        self.assertEqual(exception.args, (expect, ))
+        self.assertEqual(exception.message, 'foo %(site)s %(title)s bar')
+
+        self.assertEqual(exception.site, self.site)
+
+
+class TestUsernameExceptions(DefaultSiteTestCase):
+
+    """Test cases for username exceptions."""
+
+    dry = True
+
+    def test_username_not_specified(self):
+        """Test message of UsernameNotSpecified."""
+        exception = UsernameNotSpecified(self.site)
+        self.assertIn(
+            "usernames['{0}']['{1}'] = 'myUsername'".format(
+                self.site.family.name, self.site.code),
+            str(exception))
+
+    def test_sysopname_not_specified(self):
+        """Test message of _SysopnameNotSpecified."""
+        exception = _SysopnameNotSpecified(self.site)
+        self.assertIn(
+            "sysopnames['{0}']['{1}'] = 'myUsername'".format(
+                self.site.family.name, self.site.code),
+            str(exception))
+
+
+class TestDeprecatedExceptions(DeprecationTestCase, DefaultSiteTestCase):
 
     """Test usage of deprecation in library code."""
 
-    net = False
+    dry = False
 
     def test_UploadWarning(self):
         """Test exceptions.UploadWarning is deprecated only."""
         # Accessing from the main package should work fine.
         cls = pywikibot.UploadWarning
         self.assertNoDeprecation()
-        e = cls('foo', 'bar')
+        e = cls('foo', 'bar', site=self.site)
         self.assertIsInstance(e, pywikibot.Error)
         self.assertNoDeprecation()
 
@@ -37,7 +196,7 @@
         self.assertOneDeprecationParts('pywikibot.exceptions.UploadWarning',
                                        'pywikibot.data.api.UploadWarning')
 
-        e = cls('foo', 'bar')
+        e = cls('foo', 'bar', site=self.site)
         self.assertIsInstance(e, pywikibot.Error)
         self.assertNoDeprecation()
 
@@ -65,6 +224,25 @@
             'pywikibot.exceptions.DeprecatedPageNotFoundError')
 
 
+class TestAPIError(DefaultSiteTestCase):
+
+    """Test base APIError class."""
+
+    dry = True
+
+    def test_base(self):
+        exception = APIError('foo-code', 'foo-message', self.site)
+        self.assertEqual(str(exception), 'foo-code: foo-message')
+        self.assertEqual(unicode(exception), 'foo-code: foo-message')
+
+        self.assertEqual(exception.args, ('foo-code: foo-message', ))
+        self.assertEqual(exception.message, 'foo-code: foo-message')
+
+        self.assertEqual(exception.site, self.site)
+        self.assertEqual(exception.family, self.site.family.name)
+        self.assertEqual(exception.site_code, self.site.code)
+
+
 if __name__ == '__main__':
     try:
         unittest.main()

-- 
To view, visit https://gerrit.wikimedia.org/r/237977
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: newchange
Gerrit-Change-Id: I7f1119f2ccea48221ca400958683fd5de13ee34d
Gerrit-PatchSet: 1
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Owner: John Vandenberg <jay...@gmail.com>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to