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

Change subject: Add FlaggedRevs to Pywikibot
......................................................................

Add FlaggedRevs to Pywikibot

- APISite.FlaggedRevsMixin:
  an APISite mixin for FlaggedRevs extension
- APSite.stable_revid:
  gives the stable (reviewed) revision id for a page
- BasePage.stable_revision_id:
  a wrapper around APSite.stable_revid to give the current stable
  (reviewed) revision id of its page
- BasePage.stable_revision:
  add a convenience property method to retrieve the page’s stable
  (reviewed) revision when the FlaggedRevs extension is enabled. Returns
  a `Revision` object with content, or None if FlaggedRevs is unavailable
  or the page has no stable revision.
- flaggesrevs_tests:
  tests were added

Bug: T409848
Change-Id: I67070b06d802c0969d7b0a87c6aca3ce1d2a1f8d
---
M pywikibot/page/_basepage.py
M pywikibot/site/_apisite.py
M pywikibot/site/_extensions.py
M tests/__init__.py
A tests/flaggedrevs_tests.py
5 files changed, 244 insertions(+), 2 deletions(-)

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




diff --git a/pywikibot/page/_basepage.py b/pywikibot/page/_basepage.py
index 7dcfc50..196fea9 100644
--- a/pywikibot/page/_basepage.py
+++ b/pywikibot/page/_basepage.py
@@ -550,7 +550,9 @@
         >>> type(edit_time)
         <class 'pywikibot.time.Timestamp'>

-        .. seealso:: :attr:`oldest_revision`
+        .. seealso::
+           - :attr:`stable_revision`
+           - :attr:`oldest_revision`
         """
         rev = self._latest_cached_revision()
         if rev is not None:
@@ -561,6 +563,65 @@
         raise InvalidPageError(self)

     @property
+    @cached
+    def stable_revision_id(self) -> int | None:
+        """Return the current stable (reviewed) revision id for this page.
+
+        .. version-added:: 11.7
+        .. seealso:
+           - :attr:`stable_revision`
+           - :attr:`latest_revision_id`
+
+        :raises UnknownExtensionError: FlaggedRevs not available
+        """
+        return self.site.stable_revid(self)
+
+    @stable_revision_id.deleter
+    def stable_revision_id(self) -> None:
+        """Remove the cached latest stable revision id set for this Page."""
+        with suppress(AttributeError):
+            del self._stable_revision_id
+
+    @property
+    def stable_revision(self) -> pywikibot.page.Revision | None:
+        """Return the latest stable (reviewed) revision for this page, if any.
+
+        Uses the site’s
+        :class:`FlaggedRevsMixin<pywikibot.site._extensions.FlaggedRevsMixin>`
+        mixin to obtain a stable revid.
+
+        **Example:**
+
+        >>> site = pywikibot.Site('wikipedia:fi')
+        >>> page = pywikibot.Page(site, 'Main Page')
+        ... # get the stable revision timestamp of that page
+        >>> edit_time = page.stable_revision.timestamp
+        >>> type(edit_time)
+        <class 'pywikibot.time.Timestamp'>
+        >>> page.stable_revision.revid == site.stable_revid(page)
+        True
+        >>> # delete cache to force reloading the latest stable id
+        >>> del page.stable_revision_id
+        >>> page.stable_revision.revid == page.stable_revision_id
+        True
+
+        .. version-added:: 11.7
+        .. seealso::
+           - :attr:`stable_revision_id`
+           - :attr:`latest_revision`
+           - :attr:`oldest_revision`
+           - :attr:`APISite.stable_revid
+             <pywikibot.site._extensions.FlaggedRevsMixin.stable_revid>`
+
+        :raises UnknownExtensionError: FlaggedRevs not available
+        """
+        revid = self.stable_revision_id
+        if revid is not None:
+            return self.get_revision(revid, content=True)
+
+        return None
+
+    @property
     def text(self) -> str:
         """Return the current (edited) wikitext, loading it if necessary.

diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py
index 6965b9c..4e193d0 100644
--- a/pywikibot/site/_apisite.py
+++ b/pywikibot/site/_apisite.py
@@ -51,6 +51,7 @@
 from pywikibot.site._decorators import need_right
 from pywikibot.site._extensions import (
     EchoMixin,
+    FlaggedRevsMixin,
     GeoDataMixin,
     GlobalUsageMixin,
     LinterMixin,
@@ -100,6 +101,7 @@
 class APISite(
     BaseSite,
     EchoMixin,
+    FlaggedRevsMixin,
     GeneratorsMixin,
     GeoDataMixin,
     GlobalUsageMixin,
diff --git a/pywikibot/site/_extensions.py b/pywikibot/site/_extensions.py
index 4c2ea88..e0ca075 100644
--- a/pywikibot/site/_extensions.py
+++ b/pywikibot/site/_extensions.py
@@ -467,3 +467,35 @@
             raise Error(msg)

         return data[str(page.pageid)]['extract']
+
+
+class FlaggedRevsMixin:
+
+    """APISite mixin for the FlaggesRevs extension.
+
+    .. version-added:: 11.7
+    .. seealso:: https://www.mediawiki.org/wiki/Extension:FlaggedRevs
+    """
+
+    @need_extension('FlaggedRevs')
+    def stable_revid(self: BaseSiteProtocol,
+                     page: pywikibot.Page) -> int | None:
+        """Return the stable (reviewed) revision id for a page, if any.
+
+        :param page: The page to inspect.
+        :return: The stable revision id or None if not available.
+        :raises UnknownExtensionError: FlaggedRevs not available
+        """
+        req = self.simple_request(
+            action='query',
+            prop='flagged',
+            titles=page.title(with_section=False),
+            formatversion=2
+        )
+        data = req.submit()
+
+        pages = data.get('query', {}).get('pages', [])
+        if not pages:
+            return None
+
+        return pages[0].get('flagged', {}).get('stable_revid')
diff --git a/tests/__init__.py b/tests/__init__.py
index c84212e..79b8116 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -111,6 +111,7 @@
     'family',
     'file',
     'fixes',
+    'flaggedrevs',
     'gui',
     'http',
     'i18n',
@@ -139,8 +140,8 @@
     'site_login_logout',
     'site_obsoletesites',
     'siteinfo',
-    'superset',
     'sparql',
+    'superset',
     'tests',
     'textlib',
     'thanks',
diff --git a/tests/flaggedrevs_tests.py b/tests/flaggedrevs_tests.py
new file mode 100644
index 0000000..6766609
--- /dev/null
+++ b/tests/flaggedrevs_tests.py
@@ -0,0 +1,146 @@
+#
+# (C) Pywikibot team, 2026
+#
+# Distributed under the terms of the MIT license.
+#
+"""Unit tests for Page.stable_revision."""
+from __future__ import annotations
+
+import unittest
+from contextlib import suppress
+from unittest.mock import MagicMock, patch
+
+import pywikibot
+from pywikibot.exceptions import UnknownExtensionError
+from pywikibot.page import Revision
+from tests.aspects import TestCase
+
+
+class TestPageStableRevision(TestCase):
+
+    """Test Page.stable_revision with full mocking."""
+
+    family = 'wikipedia'
+    code = 'fi'
+    dry = True
+
+    def setUp(self) -> None:
+        """Test setup."""
+        super().setUp()
+        self.page = pywikibot.Page(self.site, 'TestPage')
+        self.page.exists = MagicMock(return_value=True)
+
+    def _mock_response(self, data: dict | list[dict]) -> dict:
+        """Helper: build API response for action=query&prop=flagged."""
+        return {
+            'query': {
+                'pages': data if isinstance(data, list) else [data]
+            }
+        }
+
+    @patch('pywikibot.site._apisite.APISite.has_extension')
+    def test_stable_revision_flaggedrevs_disabled(self, mock_has_ext):
+        """FlaggedRevs not enabled → return None."""
+        mock_has_ext.return_value = False
+
+        with self.assertRaisesRegex(
+            UnknownExtensionError,
+            'Method "stable_revid" is not implemented without the extension '
+            'FlaggedRevs'
+        ):
+            self.page.stable_revision
+
+    @patch('pywikibot.site._apisite.APISite.simple_request')
+    @patch('pywikibot.site._apisite.APISite.has_extension')
+    def test_stable_revision_no_flagged_data(self, mock_has_ext, mock_req):
+        """API returns no 'flagged' key → return None."""
+        mock_has_ext.return_value = True
+        mock_req.return_value.submit.return_value = self._mock_response(
+            {'pageid': 1, 'ns': 0, 'title': 'TestPage'}
+        )
+
+        result = self.page.stable_revision
+        self.assertIsNone(result)
+
+    @patch('pywikibot.site._apisite.APISite.simple_request')
+    @patch('pywikibot.site._apisite.APISite.has_extension')
+    def test_stable_revision_no_stable_revid(self, mock_has_ext, mock_req):
+        """'flagged' exists but no 'stable_revid' → return None."""
+        mock_has_ext.return_value = True
+        mock_req.return_value.submit.return_value = self._mock_response(
+            {
+                'pageid': 1,
+                'title': 'TestPage',
+                'flagged': {'level': 1}
+            }
+        )
+
+        result = self.page.stable_revision
+        self.assertIsNone(result)
+
+    @patch('pywikibot.site._apisite.APISite.simple_request')
+    @patch('pywikibot.site._apisite.APISite.has_extension')
+    def test_stable_revision_success(self, mock_has_ext, mock_req):
+        """Valid stable_revid → return Revision with content."""
+        mock_has_ext.return_value = True
+
+        # Mock get_revision to return a real-looking Revision
+        mock_rev = MagicMock(spec=Revision)
+        mock_rev.revid = 12345
+        mock_rev.text = 'Stable content'
+        mock_rev.user = 'Reviewer'
+        mock_rev.timestamp = pywikibot.Timestamp(2025, 1, 1)
+
+        with patch.object(self.page,
+                          'get_revision',
+                          return_value=mock_rev):
+            mock_req.return_value.submit.return_value = self._mock_response(
+                {
+                    'pageid': 1,
+                    'title': 'TestPage',
+                    'flagged': {
+                        'stable_revid': 12345,
+                        'level': 2,
+                        'pending_since': '2025-01-01T00:00:00Z'
+                    }
+                }
+            )
+
+            result = self.page.stable_revision
+
+            self.assertEqual(result, mock_rev)
+            self.assertEqual(result.revid, 12345)
+            self.assertIn('Stable content', result.text)
+
+    @patch('pywikibot.site._apisite.APISite.simple_request')
+    @patch('pywikibot.site._apisite.APISite.has_extension')
+    def test_stable_revision_multiple_pages(self, mock_has_ext, mock_req):
+        """Multiple pages in response → still find correct one."""
+        mock_has_ext.return_value = True
+
+        # Mock get_revision to return a Revision with revid=999
+        mock_rev = MagicMock(spec=Revision)
+        mock_rev.revid = 999
+        mock_rev.text = 'Other stable content'
+
+        with patch.object(self.page, 'get_revision', return_value=mock_rev):
+            mock_req.return_value.submit.return_value = self._mock_response([
+                {
+                    'pageid': 1,
+                    'title': 'TestPage',
+                    'flagged': {'stable_revid': 999}
+                },
+                {
+                    'pageid': 2,
+                    'title': 'OtherPage',
+                    'flagged': {'stable_revid': 888}
+                }
+            ])
+
+            result = self.page.stable_revision
+            self.assertEqual(result.revid, 999)
+
+
+if __name__ == '__main__':
+    with suppress(SystemExit):
+        unittest.main()

--
To view, visit 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1204707?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: I67070b06d802c0969d7b0a87c6aca3ce1d2a1f8d
Gerrit-Change-Number: 1204707
Gerrit-PatchSet: 16
Gerrit-Owner: Ezra yendau <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: Zache-tool <[email protected]>
Gerrit-Reviewer: jenkins-bot
Gerrit-CC: Tacsipacsi <[email protected]>
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to