jenkins-bot has submitted this change. ( 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1149831?usp=email )

Change subject: IMPR: replace codecs.open with pathlib.Path in 
LoginManager.readPassword
......................................................................

IMPR: replace codecs.open with pathlib.Path in LoginManager.readPassword

- use pathlib.Path in LoginManager.readPassword
- update login_tests
- update type hint for file_mode_checker

Bug: T395187
Change-Id: I25af80fd37b55e84f33fc78ad86ceacfacd6476b
---
M pywikibot/login.py
M pywikibot/tools/__init__.py
M tests/login_tests.py
3 files changed, 55 insertions(+), 25 deletions(-)

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




diff --git a/pywikibot/login.py b/pywikibot/login.py
index b73776f..f24b4fc 100644
--- a/pywikibot/login.py
+++ b/pywikibot/login.py
@@ -1,17 +1,17 @@
 """Library to log the bot in to a wiki account."""
 #
-# (C) Pywikibot team, 2003-2024
+# (C) Pywikibot team, 2003-2025
 #
 # Distributed under the terms of the MIT license.
 #
 from __future__ import annotations

-import codecs
 import datetime
 import os
 import re
 import webbrowser
 from enum import IntEnum
+from pathlib import Path
 from typing import Any
 from warnings import warn

@@ -19,7 +19,12 @@
 from pywikibot import __url__, config
 from pywikibot.comms import http
 from pywikibot.exceptions import APIError, NoUsernameError
-from pywikibot.tools import deprecated, file_mode_checker, normalize_username
+from pywikibot.tools import (
+    PYTHON_VERSION,
+    deprecated,
+    file_mode_checker,
+    normalize_username,
+)


 try:
@@ -208,23 +213,41 @@
          ('wikipedia', 'my_wikipedia_user', 'my_wikipedia_pass')
          ('en', 'wikipedia', 'my_en_wikipedia_user', 'my_en_wikipedia_pass')
          ('my_username', BotPassword('my_suffix', 'my_password'))
+
+        .. versionchanged:: 10.2
+           raises ValueError instead of AttributeError if password_file
+               is not set
+
+        :raises ValueError: `password_file` is not set in the user-config.py
+        :raises FileNotFoundError: password file does not exist
         """
+        if config.password_file is None:
+            raise ValueError('password_file is not set in the user-config.py')
+
         # Set path to password file relative to the user_config
         # but fall back on absolute path for backwards compatibility
-        assert config.base_dir is not None and config.password_file is not None
-        password_file = os.path.join(config.base_dir, config.password_file)
-        if not os.path.isfile(password_file):
-            password_file = config.password_file
+        password_path = Path(config.base_dir, config.password_file)
+
+        params = {} if PYTHON_VERSION < (3, 13) else {'follow_symlinks': False}
+        # test for symlink required for Python < 3.13
+        if not password_path.is_file(**params) or password_path.is_symlink:
+            password_path = Path(config.password_file)
+
+        # ignore this check when running tests
+        if os.environ.get('PYWIKIBOT_TEST_RUNNING', '0') == '0':
+            if not password_path.is_file(**params) or password_path.is_symlink:
+                raise FileNotFoundError(
+                    f'Password file {password_path.name} does not exist in '
+                    f'{password_path.parent}'
+                )

         # We fix password file permission first.
-        file_mode_checker(password_file, mode=config.private_files_permission)
+        file_mode_checker(password_path, mode=config.private_files_permission)

-        with codecs.open(password_file, encoding='utf-8') as f:
-            lines = f.readlines()
+        lines = password_path.read_text('utf-8').splitlines()
+        line_len = len(lines)

-        line_nr = len(lines) + 1
-        for line in reversed(lines):
-            line_nr -= 1
+        for n, line in enumerate(reversed(lines)):
             if not line.strip() or line.startswith('#'):
                 continue

@@ -234,17 +257,19 @@
                 entry = None

             if not isinstance(entry, tuple):
-                warn(f'Invalid tuple in line {line_nr}',
+                warn(f'Invalid tuple in line {line_len - n}',
                      _PasswordFileWarning)
                 continue

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

             code, family, username, password = (
-                self.site.code, self.site.family.name)[:4 - len(entry)] + entry
+                self.site.code, self.site.family.name)[:4 - entry_len] + entry
+
             if (normalize_username(username) == self.username
                     and family == self.site.family.name
                     and code == self.site.code):
diff --git a/pywikibot/tools/__init__.py b/pywikibot/tools/__init__.py
index 6eb0fe8..6e9bc35 100644
--- a/pywikibot/tools/__init__.py
+++ b/pywikibot/tools/__init__.py
@@ -659,7 +659,7 @@


 def file_mode_checker(
-    filename: str,
+    filename: str | bytes | os.PathLike,
     mode: int = 0o600,
     quiet: bool = False,
     create: bool = False
diff --git a/tests/login_tests.py b/tests/login_tests.py
index 0e95d18..48d7fd1 100755
--- a/tests/login_tests.py
+++ b/tests/login_tests.py
@@ -4,7 +4,7 @@
 e.g. used to test password-file based login.
 """
 #
-# (C) Pywikibot team, 2012-2022
+# (C) Pywikibot team, 2012-2025
 #
 # Distributed under the terms of the MIT license.
 #
@@ -12,10 +12,12 @@

 from collections import defaultdict
 from io import StringIO
+from pathlib import Path
 from unittest import mock

 from pywikibot.exceptions import NoUsernameError
 from pywikibot.login import LoginManager
+from pywikibot.tools import PYTHON_VERSION
 from tests.aspects import DefaultDrySiteTestCase, unittest


@@ -105,26 +107,29 @@

         self.stat = self.patch('os.stat')
         self.stat.return_value.st_mode = 0o100600
-
         self.chmod = self.patch('os.chmod')

-        self.open = self.patch('codecs.open')
+        if PYTHON_VERSION[:2] == (3, 10):
+            self.open = self.patch('pathlib.Path._accessor.open')
+        else:
+            self.open = self.patch('io.open')
+
         self.open.return_value = StringIO()

     def test_auto_chmod_OK(self):
         """Do not chmod files that have mode private_files_permission."""
         self.stat.return_value.st_mode = 0o100600
         LoginManager()
-        self.stat.assert_called_with(self.config.password_file)
+        self.stat.assert_called_with(Path(self.config.password_file))
         self.assertFalse(self.chmod.called)

     def test_auto_chmod_not_OK(self):
         """Chmod files that do not have mode private_files_permission."""
         self.stat.return_value.st_mode = 0o100644
         LoginManager()
-        self.stat.assert_called_with(self.config.password_file)
+        self.stat.assert_called_with(Path(self.config.password_file))
         self.chmod.assert_called_once_with(
-            self.config.password_file,
+            Path(self.config.password_file),
             0o600
         )


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

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

Reply via email to