jenkins-bot has submitted this change. ( 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/860980 )

Change subject: [IMPR] Move data.api._login.LoginManager to 
login.ClientLoginManager
......................................................................

[IMPR] Move data.api._login.LoginManager to login.ClientLoginManager

Keep LoginManagers in only one place. Therefore Move
data.api._login.LoginManager to login file as ClientLoginManager
where also OauthLoginManager resides. This might be important to
implement OAuth 2 or other login related issues like interactive
login.

Change-Id: Ic6d5bd9a5824b0400fbf4f74fbbcc324d0684ca6
---
M pywikibot/login.py
M pywikibot/data/api/__init__.py
M tests/utils.py
M pywikibot/CONTENT.rst
D pywikibot/data/api/_login.py
M pywikibot/site/_apisite.py
M tests/api_tests.py
7 files changed, 215 insertions(+), 210 deletions(-)

Approvals:
  Xqt: Looks good to me, approved
  jenkins-bot: Verified




diff --git a/pywikibot/CONTENT.rst b/pywikibot/CONTENT.rst
index 52ba21a..337b855 100644
--- a/pywikibot/CONTENT.rst
+++ b/pywikibot/CONTENT.rst
@@ -95,8 +95,6 @@
     |                            
+----------------+-------------------------------------+
     |                            | _generators.py | API/Query generators       
         |
     |                            
+----------------+-------------------------------------+
-    |                            | _login.py      | API login interface        
         |
-    |                            
+----------------+-------------------------------------+
     |                            | _optionset.py  | Boolean API option         
         |
     |                            
+----------------+-------------------------------------+
     |                            | _paraminfo.py  | API information data 
object         |
diff --git a/pywikibot/data/api/__init__.py b/pywikibot/data/api/__init__.py
index 6445c8c..c7e6db9 100644
--- a/pywikibot/data/api/__init__.py
+++ b/pywikibot/data/api/__init__.py
@@ -19,11 +19,11 @@
     QueryGenerator,
     update_page,
 )
-from pywikibot.data.api._login import LoginManager
 from pywikibot.data.api._paraminfo import ParamInfo
 from pywikibot.data.api._optionset import OptionSet
 from pywikibot.data.api._requests import CachedRequest, Request, encode_url
 from pywikibot.family import SubdomainFamily
+from pywikibot.tools import ModuleDeprecationWrapper

 __all__ = (
     'APIGeneratorBase',
@@ -31,7 +31,6 @@
     'CachedRequest',
     'ListGenerator',
     'LogEntryListGenerator',
-    'LoginManager',
     'OptionSet',
     'PageGenerator',
     'ParamInfo',
@@ -95,3 +94,9 @@


 MIMEMultipart = CTEBinaryMIMEMultipart
+
+wrapper = ModuleDeprecationWrapper(__name__)
+wrapper.add_deprecated_attr(
+    'LoginManager',
+    replacement_name='pywikibot.login.ClientLoginManager',
+    since='8.0.0')
diff --git a/pywikibot/data/api/_login.py b/pywikibot/data/api/_login.py
deleted file mode 100644
index 09a318d..0000000
--- a/pywikibot/data/api/_login.py
+++ /dev/null
@@ -1,174 +0,0 @@
-"""API login Interface."""
-#
-# (C) Pywikibot team, 2008-2022
-#
-# Distributed under the terms of the MIT license.
-#
-import datetime
-import re
-from typing import Optional
-
-import pywikibot
-from pywikibot import login
-from pywikibot.backports import Dict
-from pywikibot.login import LoginStatus
-from pywikibot.tools import deprecated
-
-__all__ = ['LoginManager']
-
-
-class LoginManager(login.LoginManager):
-
-    """Supply login_to_site method to use API interface.
-
-    .. versionchanged:: 8.0
-       2FA login was enabled.
-    """
-
-    # API login parameters mapping
-    mapping = {
-        'user': ('lgname', 'username'),
-        'password': ('lgpassword', 'password'),
-        'ldap': ('lgdomain', 'domain'),
-        'token': ('lgtoken', 'logintoken'),
-        'result': ('result', 'status'),
-        'success': ('Success', 'PASS'),
-        'fail': ('Failed', 'FAIL'),
-        'reason': ('reason', 'message')
-    }
-
-    def keyword(self, key):
-        """Get API keyword from mapping."""
-        return self.mapping[key][self.action != 'login']
-
-    def _login_parameters(self, *, botpassword: bool = False
-                          ) -> Dict[str, str]:
-        """Return login parameters."""
-        if botpassword:
-            self.action = 'login'
-        else:
-            token = self.site.tokens['login']
-            self.action = 'clientlogin'
-
-        # prepare default login parameters
-        parameters = {'action': self.action,
-                      self.keyword('user'): self.login_name,
-                      self.keyword('password'): self.password}
-
-        if self.action == 'clientlogin':
-            # clientlogin requires non-empty loginreturnurl
-            parameters['loginreturnurl'] = 'https://example.com'
-            parameters['rememberMe'] = '1'
-            parameters['logintoken'] = token
-
-        if self.site.family.ldapDomain:
-            parameters[self.keyword('ldap')] = self.site.family.ldapDomain
-
-        return parameters
-
-    def login_to_site(self) -> None:
-        """Login to the site.
-
-        Note, this doesn't do anything with cookies. The http module
-        takes care of all the cookie stuff. Throws exception on failure.
-
-        .. versionchanged:: 8.0
-           2FA login was enabled.
-        """
-        if hasattr(self, '_waituntil') \
-           and datetime.datetime.now() < self._waituntil:
-            diff = self._waituntil - datetime.datetime.now()
-            pywikibot.warning(
-                'Too many tries, waiting {} seconds before retrying.'
-                .format(diff.seconds))
-            pywikibot.sleep(diff.seconds)
-
-        self.site._loginstatus = LoginStatus.IN_PROGRESS
-
-        # Bot passwords username contains @,
-        # otherwise @ is not allowed in usernames.
-        # @ in bot password is deprecated,
-        # but we don't want to break bots using it.
-        parameters = self._login_parameters(
-            botpassword='@' in self.login_name or '@' in self.password)
-
-        # base login request
-        login_request = self.site._request(use_get=False,
-                                           parameters=parameters)
-        while True:
-            # try to login
-            try:
-                login_result = login_request.submit()
-            except pywikibot.exceptions.APIError as e:  # pragma: no cover
-                login_result = {'error': e.__dict__}
-
-            # clientlogin response can be clientlogin or error
-            if self.action in login_result:
-                response = login_result[self.action]
-                result_key = self.keyword('result')
-            elif 'error' in login_result:
-                response = login_result['error']
-                result_key = 'code'
-            else:
-                raise RuntimeError('Unexpected API login response key.')
-
-            status = response[result_key]
-            fail_reason = response.get(self.keyword('reason'), '')
-            if status == self.keyword('success'):
-                return
-
-            if status in ('NeedToken', 'WrongToken', 'badtoken'):
-                # if incorrect login token was used,
-                # force relogin and generate fresh one
-                pywikibot.error('Received incorrect login token. '
-                                'Forcing re-login.')
-                # invalidate superior wiki cookies (T224712)
-                pywikibot.data.api._invalidate_superior_cookies(
-                    self.site.family)
-                self.site.tokens.clear()
-                login_request[
-                    self.keyword('token')] = self.site.tokens['login']
-                continue
-
-            if status == 'UI':  # pragma: no cover
-                oathtoken = pywikibot.input(response['message'], password=True)
-                login_request['OATHToken'] = oathtoken
-                login_request['logincontinue'] = True
-                del login_request['username']
-                del login_request['password']
-                del login_request['rememberMe']
-                continue
-
-            # messagecode was introduced with 1.29.0-wmf.14
-            # but older wikis are still supported
-            login_throttled = response.get('messagecode') == 'login-throttled'
-
-            if (status == 'Throttled' or status == self.keyword('fail')
-                    and (login_throttled or 'wait' in fail_reason)):
-                wait = response.get('wait')
-                if wait:
-                    delta = datetime.timedelta(seconds=int(wait))
-                else:
-                    match = re.search(r'(\d+) (seconds|minutes)', fail_reason)
-                    if match:
-                        delta = datetime.timedelta(**{match[2]: int(match[1])})
-                    else:
-                        delta = datetime.timedelta()
-                self._waituntil = datetime.datetime.now() + delta
-
-            break
-
-        if 'error' in login_result:
-            raise pywikibot.exceptions.APIError(**response)
-
-        raise pywikibot.exceptions.APIError(code=status, info=fail_reason)
-
-    @deprecated("site.tokens['login']", since='8.0.0')
-    def get_login_token(self) -> Optional[str]:
-        """Fetch login token for MediaWiki 1.27+.
-
-        .. deprecated:: 8.0
-
-        :return: login token
-        """
-        return self.site.tokens['login']
diff --git a/pywikibot/login.py b/pywikibot/login.py
index 2a13ffe..1b1b661 100644
--- a/pywikibot/login.py
+++ b/pywikibot/login.py
@@ -5,7 +5,9 @@
 # Distributed under the terms of the MIT license.
 #
 import codecs
+import datetime
 import os
+import re
 import webbrowser
 from enum import IntEnum
 from typing import Any, Optional
@@ -16,7 +18,7 @@
 from pywikibot.backports import Dict, Tuple
 from pywikibot.comms import http
 from pywikibot.exceptions import APIError, NoUsernameError
-from pywikibot.tools import file_mode_checker, normalize_username
+from pywikibot.tools import deprecated, file_mode_checker, normalize_username


 try:
@@ -170,7 +172,7 @@

     def login_to_site(self) -> None:
         """Login to the site."""
-        # THIS IS OVERRIDDEN IN data/api.py
+        # This is overridden in ClientLoginManager
         raise NotImplementedError

     def storecookiedata(self) -> None:
@@ -178,30 +180,29 @@
         http.cookie_jar.save(ignore_discard=True)

     def readPassword(self) -> None:
-        """
-        Read passwords from a file.
+        """Read passwords from a file.

-        DO NOT FORGET TO REMOVE READ ACCESS FOR OTHER USERS!!!
-        Use chmod 600 password-file.
+        .. warning:: **Do not forget to remove read access for other
+           users!** Use chmod 600 for password-file.

         All lines below should be valid Python tuples in the form
-        (code, family, username, password),
-        (family, username, password) or
-        (username, password)
-        to set a default password for an username. The last matching entry will
-        be used, so default usernames should occur above specific usernames.
+        ``(code, family, username, password)``,
+        ``(family, username, password)`` or ``(username, password)`` to
+        set a default password for an username. The last matching entry
+        will be used, so default usernames should occur above specific
+        usernames.

-        For BotPasswords the password should be given as a BotPassword object.
+        .. note:: For BotPasswords the password should be given as a
+           :class:`BotPassword` object.

         The file must be either encoded in ASCII or UTF-8.

-        Example::
+        **Example**::

          ('my_username', 'my_default_password')
          ('wikipedia', 'my_wikipedia_user', 'my_wikipedia_pass')
          ('en', 'wikipedia', 'my_en_wikipedia_user', 'my_en_wikipedia_pass')
-         ('my_username', BotPassword(
-          'my_BotPassword_suffix', 'my_BotPassword_password'))
+         ('my_username', BotPassword('my_suffix', 'my_password'))
         """
         # Set path to password file relative to the user_config
         # but fall back on absolute path for backwards compatibility
@@ -215,6 +216,7 @@

         with codecs.open(password_file, encoding='utf-8') as f:
             lines = f.readlines()
+
         line_nr = len(lines) + 1
         for line in reversed(lines):
             line_nr -= 1
@@ -232,8 +234,8 @@
                 continue

             if not 2 <= len(entry) <= 4:
-                warn('The length of tuple in line {} should be 2 to 4 ({} '
-                     'given)'.format(line_nr, entry), _PasswordFileWarning)
+                warn(f'The length of tuple in line {line_nr} should be 2 to 4 '
+                     f'({entry} given)', _PasswordFileWarning)
                 continue

             code, family, username, password = (
@@ -313,6 +315,164 @@
         return False


+class ClientLoginManager(LoginManager):
+
+    """Supply login_to_site method to use API interface.
+
+    .. versionchanged:: 8.0
+       2FA login was enabled. LoginManager was moved from :mod:`data.api`
+       to :mod:`login` module and renamed to *ClientLoginManager*.
+    """
+
+    # API login parameters mapping
+    mapping = {
+        'user': ('lgname', 'username'),
+        'password': ('lgpassword', 'password'),
+        'ldap': ('lgdomain', 'domain'),
+        'token': ('lgtoken', 'logintoken'),
+        'result': ('result', 'status'),
+        'success': ('Success', 'PASS'),
+        'fail': ('Failed', 'FAIL'),
+        'reason': ('reason', 'message')
+    }
+
+    def keyword(self, key):
+        """Get API keyword from mapping."""
+        return self.mapping[key][self.action != 'login']
+
+    def _login_parameters(self, *, botpassword: bool = False
+                          ) -> Dict[str, str]:
+        """Return login parameters."""
+        if botpassword:
+            self.action = 'login'
+        else:
+            token = self.site.tokens['login']
+            self.action = 'clientlogin'
+
+        # prepare default login parameters
+        parameters = {'action': self.action,
+                      self.keyword('user'): self.login_name,
+                      self.keyword('password'): self.password}
+
+        if self.action == 'clientlogin':
+            # clientlogin requires non-empty loginreturnurl
+            parameters['loginreturnurl'] = 'https://example.com'
+            parameters['rememberMe'] = '1'
+            parameters['logintoken'] = token
+
+        if self.site.family.ldapDomain:
+            parameters[self.keyword('ldap')] = self.site.family.ldapDomain
+
+        return parameters
+
+    def login_to_site(self) -> None:
+        """Login to the site.
+
+        Note, this doesn't do anything with cookies. The http module
+        takes care of all the cookie stuff. Throws exception on failure.
+
+        .. versionchanged:: 8.0
+           2FA login was enabled.
+        """
+        if hasattr(self, '_waituntil') \
+           and datetime.datetime.now() < self._waituntil:
+            diff = self._waituntil - datetime.datetime.now()
+            pywikibot.warning(
+                'Too many tries, waiting {} seconds before retrying.'
+                .format(diff.seconds))
+            pywikibot.sleep(diff.seconds)
+
+        self.site._loginstatus = LoginStatus.IN_PROGRESS
+
+        # Bot passwords username contains @,
+        # otherwise @ is not allowed in usernames.
+        # @ in bot password is deprecated,
+        # but we don't want to break bots using it.
+        parameters = self._login_parameters(
+            botpassword='@' in self.login_name or '@' in self.password)
+
+        # base login request
+        login_request = self.site._request(use_get=False,
+                                           parameters=parameters)
+        while True:
+            # try to login
+            try:
+                login_result = login_request.submit()
+            except pywikibot.exceptions.APIError as e:  # pragma: no cover
+                login_result = {'error': e.__dict__}
+
+            # clientlogin response can be clientlogin or error
+            if self.action in login_result:
+                response = login_result[self.action]
+                result_key = self.keyword('result')
+            elif 'error' in login_result:
+                response = login_result['error']
+                result_key = 'code'
+            else:
+                raise RuntimeError('Unexpected API login response key.')
+
+            status = response[result_key]
+            fail_reason = response.get(self.keyword('reason'), '')
+            if status == self.keyword('success'):
+                return
+
+            if status in ('NeedToken', 'WrongToken', 'badtoken'):
+                # if incorrect login token was used,
+                # force relogin and generate fresh one
+                pywikibot.error('Received incorrect login token. '
+                                'Forcing re-login.')
+                # invalidate superior wiki cookies (T224712)
+                pywikibot.data.api._invalidate_superior_cookies(
+                    self.site.family)
+                self.site.tokens.clear()
+                login_request[
+                    self.keyword('token')] = self.site.tokens['login']
+                continue
+
+            if status == 'UI':  # pragma: no cover
+                oathtoken = pywikibot.input(response['message'], password=True)
+                login_request['OATHToken'] = oathtoken
+                login_request['logincontinue'] = True
+                del login_request['username']
+                del login_request['password']
+                del login_request['rememberMe']
+                continue
+
+            # messagecode was introduced with 1.29.0-wmf.14
+            # but older wikis are still supported
+            login_throttled = response.get('messagecode') == 'login-throttled'
+
+            if (status == 'Throttled' or status == self.keyword('fail')
+                    and (login_throttled or 'wait' in fail_reason)):
+                wait = response.get('wait')
+                if wait:
+                    delta = datetime.timedelta(seconds=int(wait))
+                else:
+                    match = re.search(r'(\d+) (seconds|minutes)', fail_reason)
+                    if match:
+                        delta = datetime.timedelta(**{match[2]: int(match[1])})
+                    else:
+                        delta = datetime.timedelta()
+                self._waituntil = datetime.datetime.now() + delta
+
+            break
+
+        if 'error' in login_result:
+            raise pywikibot.exceptions.APIError(**response)
+
+        raise pywikibot.exceptions.APIError(code=status, info=fail_reason)
+
+    @deprecated("site.tokens['login']", since='8.0.0')
+    def get_login_token(self) -> Optional[str]:
+        """Fetch login token for MediaWiki 1.27+.
+
+        .. deprecated:: 8.0
+
+        :return: login token
+        """
+        return self.site.tokens['login']
+
+
 class BotPassword:

     """BotPassword object for storage in password file."""
diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py
index 8032203..10f54a3 100644
--- a/pywikibot/site/_apisite.py
+++ b/pywikibot/site/_apisite.py
@@ -53,7 +53,7 @@
     TitleblacklistError,
     UnknownExtensionError,
 )
-from pywikibot.login import LoginStatus as _LoginStatus
+from pywikibot import login
 from pywikibot.site._basesite import BaseSite
 from pywikibot.site._decorators import need_right, need_version
 from pywikibot.site._extensions import (
@@ -126,7 +126,7 @@
         super().__init__(code, fam, user)
         self._globaluserinfo: Dict[Union[int, str], Any] = {}
         self._interwikimap = _InterwikiMap(self)
-        self._loginstatus = _LoginStatus.NOT_ATTEMPTED
+        self._loginstatus = login.LoginStatus.NOT_ATTEMPTED
         self._msgcache: Dict[str, str] = {}
         self._paraminfo = api.ParamInfo(self)
         self._siteinfo = Siteinfo(self)
@@ -354,7 +354,7 @@
         #       (below) is successful. Instead, log the problem,
         #       to be increased to 'warning' level once majority
         #       of issues are resolved.
-        if self._loginstatus == _LoginStatus.IN_PROGRESS:
+        if self._loginstatus == login.LoginStatus.IN_PROGRESS:
             pywikibot.log(
                 '{!r}.login() called when a previous login was in progress.'
                 .format(self))
@@ -364,18 +364,18 @@
         # logged_in() is False if _userinfo exists, which means this
         # will have no effect for the invocation from api.py
         if self.logged_in():
-            self._loginstatus = _LoginStatus.AS_USER
+            self._loginstatus = login.LoginStatus.AS_USER
             return

         # check whether a login cookie already exists for this user
         # or check user identity when OAuth enabled
-        self._loginstatus = _LoginStatus.IN_PROGRESS
+        self._loginstatus = login.LoginStatus.IN_PROGRESS
         if user:
             self._username = normalize_username(user)
         try:
             del self.userinfo  # force reload
             if self.userinfo['name'] == self.user():
-                self._loginstatus = _LoginStatus.AS_USER
+                self._loginstatus = login.LoginStatus.AS_USER
                 return

         # May occur if you are not logged in (no API read permissions).
@@ -406,14 +406,15 @@

             raise NoUsernameError(error_msg)

-        login_manager = api.LoginManager(site=self, user=self.username())
+        login_manager = login.ClientLoginManager(site=self,
+                                                 user=self.username())
         if login_manager.login(retry=True, autocreate=autocreate):
             self._username = login_manager.username
             del self.userinfo  # force reloading

             # load userinfo
             if self.userinfo['name'] == self.username():
-                self._loginstatus = _LoginStatus.AS_USER
+                self._loginstatus = login.LoginStatus.AS_USER
                 return

             pywikibot.error('{} != {} after {}.login() and successful '
@@ -423,7 +424,7 @@
                                     type(self).__name__,
                                     type(login_manager).__name__))

-        self._loginstatus = _LoginStatus.NOT_LOGGED_IN  # failure
+        self._loginstatus = login.LoginStatus.NOT_LOGGED_IN  # failure
 
     def _relogin(self) -> None:
         """Force a login sequence without logging out, using the current user.
@@ -433,7 +434,7 @@
         from the site.
         """
         del self.userinfo
-        self._loginstatus = _LoginStatus.NOT_LOGGED_IN
+        self._loginstatus = login.LoginStatus.NOT_LOGGED_IN
         self.login()

     def logout(self) -> None:
@@ -452,7 +453,7 @@
         req_params = {'action': 'logout', 'token': self.tokens['csrf']}
         uirequest = self.simple_request(**req_params)
         uirequest.submit()
-        self._loginstatus = _LoginStatus.NOT_LOGGED_IN
+        self._loginstatus = login.LoginStatus.NOT_LOGGED_IN

         # Reset tokens and user properties
         del self.userinfo
diff --git a/tests/api_tests.py b/tests/api_tests.py
index c721cb3..5f13c04 100755
--- a/tests/api_tests.py
+++ b/tests/api_tests.py
@@ -847,12 +847,12 @@
     def setUp(self):
         """Patch the LoginManager to avoid UI interaction."""
         super().setUp()
-        self.orig_login_manager = pywikibot.data.api.LoginManager
-        pywikibot.data.api.LoginManager = FakeLoginManager
+        self.orig_login_manager = pywikibot.login.ClientLoginManager
+        pywikibot.login.ClientLoginManager = FakeLoginManager

     def tearDown(self):
         """Restore the original LoginManager."""
-        pywikibot.data.api.LoginManager = self.orig_login_manager
+        pywikibot.login.ClientLoginManager = self.orig_login_manager
         super().tearDown()

     @patch.object(pywikibot, 'info')
diff --git a/tests/utils.py b/tests/utils.py
index 397552d..c71658d 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -428,7 +428,7 @@
         return self._disambig


-class FakeLoginManager(pywikibot.data.api.LoginManager):
+class FakeLoginManager(pywikibot.login.ClientLoginManager):

     """Loads a fake password."""


--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/860980
To unsubscribe, or for help writing mail filters, visit 
https://gerrit.wikimedia.org/r/settings

Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: Ic6d5bd9a5824b0400fbf4f74fbbcc324d0684ca6
Gerrit-Change-Number: 860980
Gerrit-PatchSet: 7
Gerrit-Owner: Xqt <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
Gerrit-CC: Mpaa <[email protected]>
Gerrit-MessageType: merged
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to