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

Change subject: [fix] use WikiHistory instead of XTools to implement 
authorship()
......................................................................

[fix] use WikiHistory instead of XTools to implement authorship()

XTools/authorship uses HTTP status 403 (Forbidden) instead of
429 (Client Error) and login is required but not implemented yet.
See T382998 for authentification request.

Bug: T392694
Change-Id: Ia35eca0ee4456f3c0ea0bd99a423096c431f9181
---
M pywikibot/page/_toolforge.py
M tests/wikiblame_tests.py
2 files changed, 110 insertions(+), 117 deletions(-)

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




diff --git a/pywikibot/page/_toolforge.py b/pywikibot/page/_toolforge.py
index 65407b8..973cd72 100644
--- a/pywikibot/page/_toolforge.py
+++ b/pywikibot/page/_toolforge.py
@@ -3,7 +3,7 @@
 .. versionadded:: 7.7
 """
 #
-# (C) Pywikibot team, 2022-2024
+# (C) Pywikibot team, 2022-2025
 #
 # Distributed under the terms of the MIT license.
 #
@@ -12,23 +12,10 @@
 import collections
 import re
 from http import HTTPStatus
-from typing import TYPE_CHECKING
+from warnings import warn

 import pywikibot
-from pywikibot import textlib
-from pywikibot.tools import deprecated, deprecated_args
-
-
-try:
-    import wikitextparser
-except ImportError as e:
-    wikitextparser = e
-
-if TYPE_CHECKING:
-    import datetime
-
-    from pywikibot import Timestamp
-    DATETYPE = str | Timestamp | datetime.datetime | datetime.date | None
+from pywikibot.tools import deprecated, deprecated_args, remove_last_args


 class WikiBlameMixin:
@@ -39,10 +26,7 @@
     """

     #: Supported wikipedia site codes
-    WIKIBLAME_CODES = (
-        'ar', 'de', 'en', 'es', 'eu', 'fr', 'hu', 'id', 'it', 'ja', 'nl', 'pl',
-        'pt', 'tr',
-    )
+    WIKIBLAME_CODES = 'als', 'bar', 'de', 'en', 'it', 'nds', 'sco'

     def _check_wh_supported(self):
         """Check if WikiHistory is supported."""
@@ -50,20 +34,17 @@
             raise NotImplementedError(
                 'main_authors method is implemented for wikipedia family only')

-        if self.site.code not in self.WIKIBLAME_CODES:
-            raise NotImplementedError('main_authors method is not implemented '
-                                      f'for wikipedia:{self.site.code}')
-
-        if self.namespace() != pywikibot.site.Namespace.MAIN:
+        if (code := self.site.code) not in self.WIKIBLAME_CODES:
             raise NotImplementedError(
-                'main_authors method is implemented for main namespace only')
+                f'main_authors method is not implemented for wikipedia:{code}')
+
+        if (ns := self.namespace()) not in (0, 4, 10, 12, 14, 100):
+            raise NotImplementedError(
+                f'main_authors method is not implemented for {ns} namespace')

         if not self.exists():
             raise pywikibot.exceptions.NoPageError(self)

-        if isinstance(wikitextparser, ImportError):
-            raise wikitextparser
-
     @deprecated('authorsship', since='9.3.0')
     @deprecated_args(onlynew=None)  # since 9.2.0
     def main_authors(self) -> collections.Counter[str, int]:
@@ -72,40 +53,27 @@
         Sample:

         >>> import pywikibot
-        >>> site = pywikibot.Site('wikipedia:eu')
-        >>> page = pywikibot.Page(site, 'Python (informatika)')
-        >>> auth = page.main_authors()  # doctest: +SKIP
-        >>> auth.most_common(1)  # doctest: +SKIP
-        [('Ksarasola', 82)]
+        >>> site = pywikibot.Site('wikipedia:de')
+        >>> page = pywikibot.Page(site, 'Project:Pywikibot')
+        >>> auth = page.main_authors()
+        >>> auth.most_common(1)
+        [('DrTrigon', 37)]

-        .. important:: Only implemented for main namespace pages and
-           only wikipedias of :attr:`WIKIBLAME_CODES` are supported.
-        .. seealso::
-           - https://wikihistory.toolforge.org
-           - https://de.wikipedia.org/wiki/Wikipedia:Technik/Cloud/wikihistory
-           - https://xtools.wmcloud.org/authorship/
-
-        .. versionchanged:: 9.2
-           do not use any wait cycles due to :phab:`366100`.
-        .. versionchanged:: 9.3
-           https://xtools.wmcloud.org/authorship/ is used to retrieve
-           authors
         .. deprecated:: 9.3
            use :meth:`authorship` instead.
+        .. seealso:: :meth:`authorship` for further informations

         :return: Percentage of edits for each username

-        :raise ImportError: missing ``wikitextparser`` module.
         :raise NotImplementedError: unsupported site or unsupported
             namespace.
-        :raise Error: Error response from xtools.
         :raise NoPageError: The page does not exist.
-        :raise requests.exceptions.HTTPError: 429 Client Error: Too Many
-            Requests for url; login to meta family first.
+        :raise TimeoutError: WikiHistory timeout
         """
         return collections.Counter(
             {user: int(cnt) for user, (_, cnt) in self.authorship(5).items()})

+    @remove_last_args(['revid', 'date'])  # since 10.1
     def authorship(
         self,
         n: int | None = None,
@@ -113,13 +81,11 @@
         min_chars: int = 0,
         min_pct: float = 0.0,
         max_pct_sum: float | None = None,
-        revid: int | None = None,
-        date: DATETYPE = None,
     ) -> dict[str, tuple[int, float]]:
         """Retrieve authorship attribution of an article.

-        This method uses XTools/Authorship to retrieve the authors
-        measured by character count.
+        This method uses WikiHistory to retrieve the authors measured by
+        character count.

         Sample:

@@ -130,15 +96,58 @@
         >>> auth  # doctest: +SKIP
         {'1234qwer1234qwer4': (68, 100.0)}

-        .. important:: Only implemented for main namespace pages and
-           only wikipedias of :attr:`WIKIBLAME_CODES` are supported.
-        .. seealso::
-           - https://xtools.wmcloud.org/authorship/
-           - https://www.mediawiki.org/wiki/XTools/Authorship
-           - https://www.mediawiki.org/wiki/WikiWho
-
+        .. important:: Only implemented for pages in Main, Project,
+           Category and Template namespaces and only wikipedias of
+           :attr:`WIKIBLAME_CODES` are supported.
         .. versionadded:: 9.3
-           this method replaces :meth:`main_authors`.
+           XTools is used to retrieve authors. This method replaces
+           :meth:`main_authors`.
+        .. versionchanged:: 10.1
+           WikiHistory is used to retrieve authors due to :phab:`T392694`.
+
+        Here are the differences between these two implementations:
+
+        .. tabs::
+
+           .. tab:: WikiHistory
+
+              .. versionadded:: 10.1
+
+              - Implemented from version 7.7 until 9.2 (with
+                :meth:`main_authors` method) and from 10.1.
+              - Main, Project, Category and Template namespaces are
+                supported
+              - Only 'als', 'bar', 'de', 'en', 'it', 'nds' and 'sco'
+                Wikipedias are supported.
+              - Revision ID *revid* or revision *date* is not supported.
+                Always the latest revision is used.
+              - Only the most 5 authors are given.
+              - No additional parsing library is required.
+
+
+              .. seealso::
+                 - https://wikihistory.toolforge.org
+                 - https://de.wikipedia.org/wiki/WP:HT/wikihistory
+
+           .. tab:: XTools
+
+              .. versionremoved:: 10.1
+
+              - Implemented from version 9.3 until 10.0.
+              - Only Main namespace is supported.
+              - Only 'ar', 'de', 'en', 'es', 'eu', 'fr', 'hu', 'id',
+                'it', 'ja', 'nl', 'pl', 'pt' and 'tr' Wikipedias are
+                supported.
+              - Revision ID *revid* or revision *date* is supported to
+                get authorship for this revision.
+              - All authors can be given.
+              - wikitextparser parsing library is required.
+
+              .. seealso::
+                 - https://xtools.wmcloud.org/authorship/
+                 - https://www.mediawiki.org/wiki/XTools/Authorship
+                 - https://www.mediawiki.org/wiki/WikiWho
+

         :param n: Only return the first *n* or fewer authors.
         :param min_chars: Only return authors with more than *min_chars*
@@ -157,68 +166,49 @@
         :return: Character count and percentage of edits for each
             username.

-        :raise ImportError: missing ``wikitextparser`` module
         :raise NotImplementedError: unsupported site or unsupported
             namespace.
-        :raise Error: Error response from xtools.
-        :raiseNoPageError: The page does not exist.
-        :raise requests.exceptions.HTTPError: 429 Client Error: Too Many
-            Requests for url; login to meta family first.
+        :raise NoPageError: The page does not exist.
+        :raise TimeoutError: WikiHistory timeout
         """
-        baseurl = 'https://xtools.wmcloud.org/authorship/{url}&format=wikitext'
-        pattern = r'\[\[.+[|/](?P<user>.+)\]\]'
+        if n and n > 5:
+            warn('Only the first 5 authors can be given.')
+
+        baseurl = 'https://wikihistory.toolforge.org'
+        pattern = (r'><bdi>(?P<author>.+?)</bdi></a>\s'
+                   r'\((?P<percent>\d{1,3})&')

         self._check_wh_supported()

-        if revid and date:
-            raise ValueError(
-                'You cannot specify revid together with date argument')
+        for onlynew in (1, 0):
+            url = baseurl + (f'/wiki/getauthors.php?wiki={self.site.code}wiki'
+                             f'&page_id={self.pageid}&onlynew={onlynew}')

-        show = revid or 0 if date is None else str(date)[:10]
-        url = '{}.wikipedia.org/{}/{}?uselang={}'.format(
-            self.site.code,
-            self.title(as_url=True, with_ns=False, with_section=False),
-            show,
-            'en',
-        )
-        url = baseurl.format(url=url)
+            r = pywikibot.comms.http.fetch(url)
+            if r.status_code != HTTPStatus.OK:
+                r.raise_for_status()

-        r = pywikibot.comms.http.fetch(url)
-        if r.status_code != HTTPStatus.OK:
-            r.raise_for_status()
+            if 'Timeout' not in r.text:
+                break

+            pywikibot.sleep(pywikibot.config.retry_wait)
+        else:
+            raise pywikibot.exceptions.TimeoutError('WikiHistory Timeout')
+
+        length = len(self.text)
         result: list[list[str]] = []
-        try:
-            table = wikitextparser.parse(r.text).tables[0]
-        except IndexError:
-            pattern = textlib.get_regexes('code')[0]
-            match = pattern.search(r.text)
-            if match:
-                msg = textlib.removeHTMLParts(match[0])
-            else:
-                pattern = textlib.get_regexes('strong')[0]
-                strongs = pattern.findall(r.text)
-                if strongs:
-                    msg = textlib.removeHTMLParts('\n'.join(strongs))
-                else:
-                    msg = 'Unknown exception from xtools'
-            raise pywikibot.exceptions.Error(msg) from None
-
         pct_sum = 0.0
-        for row in table.data():
-            if row[0] == 'Rank':
-                continue  # skip headline
-
-            rank = int(row[0])
-            user = re.match(pattern, row[1])['user']
-            chars = int(row[3].replace(',', '_'))
-            percent = float(row[4].rstrip('%'))
+        for rank, (user, cnt) in enumerate(re.findall(pattern, r.text),
+                                           start=1):
+            chars = length * int(cnt) // 100
+            percent = float(cnt)

             # take into account that data() is ordered
             if n and rank > n or chars < min_chars or percent < min_pct:
                 break

             result.append((user, chars, percent))
+
             pct_sum += percent
             if max_pct_sum and pct_sum >= max_pct_sum:
                 break
diff --git a/tests/wikiblame_tests.py b/tests/wikiblame_tests.py
index 0fdb2ab..aaa70e2 100644
--- a/tests/wikiblame_tests.py
+++ b/tests/wikiblame_tests.py
@@ -11,7 +11,7 @@
 from contextlib import suppress

 import pywikibot
-from tests.aspects import TestCase, require_modules
+from tests.aspects import TestCase


 class TestWikiBlameMixin(TestCase):
@@ -19,7 +19,7 @@
     """Test WikiBlameMixin using nds wiki."""

     family = 'wikipedia'
-    code = 'nl'
+    code = 'nds'

     def test_exceptions(self):
         """Test that main_authors fails if page does not exist."""
@@ -29,27 +29,30 @@
                                     f"Page {title} doesn't exist"):
             page.authorship()

-        page = pywikibot.Page(self.site, 'Project:Pywikibot')
+        page = pywikibot.Page(self.site, 'Diskuschoon:Wikipedia')
         with self.assertRaisesRegex(
             NotImplementedError,
-                'main_authors method is implemented for main namespace only'):
+                'main_authors method is not implemented for Talk: namespace'):
             page.authorship()

-    @unittest.expectedFailure  # T392694
-    @require_modules('wikitextparser')
+        page = pywikibot.Page(pywikibot.Site('wikipedia:nl'),
+                              'Project:Pywikibot')
+        with self.assertRaisesRegex(
+            NotImplementedError,
+                'main_authors method is not implemented for wikipedia:nl'):
+            page.authorship()
+
     def test_main_authors(self):
         """Test main_authors() method."""
-        page = pywikibot.Page(self.site, 'Python (programmeertaal)')
+        page = pywikibot.Page(self.site, 'Python (Programmeerspraak)')
         auth = page.authorship(5)
         self.assertLessEqual(len(auth), 5)
         self.assertLessEqual(sum(pct for _, pct in auth.values()), 100)
         user, values = next(iter(auth.items()))
-        self.assertEqual(user, 'Emperor045')
+        self.assertEqual(user, 'RebeccaBreu')
         self.assertIsInstance(values[0], int)
         self.assertIsInstance(values[1], float)

-    @unittest.expectedFailure  # T392694
-    @require_modules('wikitextparser')
     def test_restrictions(self):
         """Test main_authors() method with restrictions."""
         page = pywikibot.Page(pywikibot.Site('wikipedia:en'), 'Python')

--
To view, visit 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1147141?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: Ia35eca0ee4456f3c0ea0bd99a423096c431f9181
Gerrit-Change-Number: 1147141
Gerrit-PatchSet: 1
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