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

Change subject: Support Scribunto redirects
......................................................................

Support Scribunto redirects

MediaWiki 1.42 recognizes Scribunto redirects written as Lua, but
Page.set_redirect_target() always serialized redirects as wikitext.
Resolving a double redirect could therefore turn a valid module into
invalid Lua.

Add content-model-aware matching and formatting while retaining the
existing wikitext redirect_regex. Reuse these helpers when changing Page
targets and processing dumps, whose revision content model is now retained.
Cover localized Module titles and reject cross-model syntax.

Bug: T417129
Change-Id: I03e1afaac383b7e5715c33c5224a35343d607665
---
M pywikibot/page/_page.py
M pywikibot/site/_basesite.py
M pywikibot/xmlreader.py
M scripts/redirect.py
M tests/basesite_tests.py
M tests/page_tests.py
M tests/redirect_bot_tests.py
M tests/xmlreader_tests.py
8 files changed, 220 insertions(+), 9 deletions(-)

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




diff --git a/pywikibot/page/_page.py b/pywikibot/page/_page.py
index c06cf96..11c4ac3 100644
--- a/pywikibot/page/_page.py
+++ b/pywikibot/page/_page.py
@@ -160,6 +160,8 @@

         .. version-changed:: 9.3
            *botflag* keyword parameter was renamed to *bot*.
+        .. version-changed:: next
+           Scribunto redirects use their Lua redirect syntax.

         :param target_page: Target of the redirect, this argument is
             required.
@@ -184,8 +186,10 @@
         if self.exists() and not self.isRedirectPage() and not force:
             raise IsNotRedirectPageError(self)

+        content_model = self.content_model
         old_text = self.text
-        result = self.site.redirect_regex.search(old_text)
+        result = self.site.redirect_match(
+            old_text, content_model=content_model)
         if result:
             oldlink = result[1]
             if (keep_section and '#' in oldlink
@@ -200,10 +204,9 @@
         else:
             prefix = suffix = ''

-        target_link = target_page.title(as_link=True, textlink=True,
-                                        allow_interwiki=False)
-        target_link = f'#{self.site.redirect()} {target_link}'
-        self.text = prefix + target_link + suffix
+        redirect_text = self.site.format_redirect(
+            target_page, content_model=content_model)
+        self.text = prefix + redirect_text + suffix
         if save:
             self.save(**kwargs)

diff --git a/pywikibot/site/_basesite.py b/pywikibot/site/_basesite.py
index 5096e66..0259778 100644
--- a/pywikibot/site/_basesite.py
+++ b/pywikibot/site/_basesite.py
@@ -424,6 +424,54 @@
         return re.compile(fr'\s*#(?:{tags})\s*:?\s*\[\[(.+?)(?:\|.*?)?\]\]',
                           re.IGNORECASE | re.DOTALL)

+    def redirect_match(
+        self,
+        text: str,
+        *,
+        content_model: str | None = 'wikitext',
+    ) -> re.Match[str] | None:
+        """Return a match object for redirect *text*.
+
+        Group 1 in the match object is the redirect target. Matching is
+        selected by *content_model*; unrecognized models use wikitext syntax.
+
+        .. version-added:: next
+
+        :param text: Text to match.
+        :param content_model: Content model used to interpret *text*.
+        :return: Match object whose first group is the redirect target, or
+            ``None`` if *text* is not a redirect.
+        """
+        if content_model == 'Scribunto':
+            return re.fullmatch(r'return require \[\[(.*?)\]\]', text)
+        return self.redirect_regex.search(text)
+
+    def format_redirect(
+        self,
+        target_page: pywikibot.Page,
+        *,
+        content_model: str | None = 'wikitext',
+    ) -> str:
+        """Return redirect text pointing to *target_page*.
+
+        Formatting is selected by *content_model*; unrecognized models use
+        wikitext syntax.
+
+        .. version-added:: next
+
+        :param target_page: Target page of the redirect.
+        :param content_model: Content model used to format the redirect.
+        """
+        target_link = target_page.title(
+            as_link=True,
+            textlink=True,
+            allow_interwiki=False,
+            with_section=content_model != 'Scribunto',
+        )
+        if content_model == 'Scribunto':
+            return f'return require {target_link}'
+        return f'#{self.redirect()} {target_link}'
+
     def sametitle(self, title1: str, title2: str) -> bool:
         """Return True if title1 and title2 identify the same wiki page.

diff --git a/pywikibot/xmlreader.py b/pywikibot/xmlreader.py
index 5887384..fc1c8e9 100644
--- a/pywikibot/xmlreader.py
+++ b/pywikibot/xmlreader.py
@@ -40,7 +40,11 @@
 @dataclass
 class XmlEntry:

-    """Represent a page."""
+    """Represent a page.
+
+    .. version-changed:: next
+       The *content_model* field was added.
+    """

     # TODO: there are more tags we can read.
     title: str
@@ -55,6 +59,7 @@
     revisionid: str
     comment: str
     isredirect: bool
+    content_model: str = 'wikitext'


 class Headers(NamedTuple):
@@ -323,6 +328,7 @@
             timestamp=revision.findtext(f'{uri}timestamp'),
             revisionid=revision.findtext(f'{uri}id'),
             comment=revision.findtext(f'{uri}comment'),
+            content_model=(revision.findtext(f'{uri}model') or 'wikitext'),
             # could get comment, minor as well
         )

diff --git a/scripts/redirect.py b/scripts/redirect.py
index 44eec6c..4cf812e 100755
--- a/scripts/redirect.py
+++ b/scripts/redirect.py
@@ -160,7 +160,6 @@
         redict = {}
         # open xml dump and read page titles out of it
         dump = xmlreader.XmlDump(xmlFilename, on_error=pywikibot.error)
-        redirR = self.site.redirect_regex
         readPagesCount = 0
         pageTitles = set()
         for entry in dump.parse():
@@ -177,7 +176,10 @@
                 pageTitles.add(space_to_underscore(pywikibot.Link(entry.title,
                                                                   self.site)))

-            m = redirR.match(entry.text)
+            m = None
+            if entry.isredirect:
+                m = self.site.redirect_match(
+                    entry.text, content_model=entry.content_model)
             if m:
                 target = m[1]
                 # There might be redirects to another wiki. Ignore these.
diff --git a/tests/basesite_tests.py b/tests/basesite_tests.py
index 1d3e276..2a4b717 100755
--- a/tests/basesite_tests.py
+++ b/tests/basesite_tests.py
@@ -66,6 +66,54 @@
         self.assertEqual(mysite.doc_subpage, self.sites[key]['result'])


+class TestRedirectMethods(DefaultSiteTestCase):
+
+    """Test content-model-aware redirect methods."""
+
+    dry = True
+
+    def test_redirect_match(self) -> None:
+        """Test redirect matching for supported content models."""
+        tests = (
+            ('#REDIRECT [[Target]]', 'wikitext', 'Target'),
+            ('return require [[Module:Target]]',
+             'Scribunto', 'Module:Target'),
+        )
+        for text, content_model, target in tests:
+            with self.subTest(content_model=content_model):
+                match = self.site.redirect_match(
+                    text, content_model=content_model)
+                self.assertIsNotNone(match)
+                self.assertEqual(match[1], target)
+
+    def test_redirect_match_uses_content_model(self) -> None:
+        """Test that redirect syntax is not applied to another model."""
+        self.assertIsNone(self.site.redirect_match(
+            'return require [[Module:Target]]',
+            content_model='wikitext',
+        ))
+        self.assertIsNone(self.site.redirect_match(
+            '#REDIRECT [[Target]]',
+            content_model='Scribunto',
+        ))
+        self.assertIsNone(self.site.redirect_match(
+            'Return require [[Module:Target]]',
+            content_model='Scribunto',
+        ))
+
+    def test_format_redirect(self) -> None:
+        """Test redirect formatting for supported content models."""
+        target = pywikibot.Page(self.site, 'Target#section')
+        self.assertEqual(
+            self.site.format_redirect(target),
+            f'#{self.site.redirect()} [[Target#section]]',
+        )
+        self.assertEqual(
+            self.site.format_redirect(target, content_model='Scribunto'),
+            'return require [[Target]]',
+        )
+
+
 class TestSiteObject(DefaultSiteTestCase):

     """Test cases for Site methods."""
diff --git a/tests/page_tests.py b/tests/page_tests.py
index 1a0e72f..c044bd0 100755
--- a/tests/page_tests.py
+++ b/tests/page_tests.py
@@ -887,6 +887,74 @@
         self.assertEqual(f'{self.page!r}', "Page('Ō')")


+class TestSetRedirectTarget(DefaultSiteTestCase):
+
+    """Test Page.set_redirect_target without network access."""
+
+    dry = True
+
+    def setUp(self) -> None:
+        """Add a localized Module namespace to the dry site."""
+        super().setUp()
+        self._namespaces = self.site.namespaces
+        namespaces = dict(self._namespaces)
+        namespaces[828] = pywikibot.site.Namespace(
+            828,
+            custom_name='Modulo',
+            canonical_name='Module',
+            defaultcontentmodel='Scribunto',
+            case='first-letter',
+        )
+        self.site._namespaces = pywikibot.site.NamespacesDict(namespaces)
+
+    def tearDown(self) -> None:
+        """Restore the dry site's namespaces."""
+        self.site._namespaces = self._namespaces
+        super().tearDown()
+
+    def _redirect(self, title: str, text: str,
+                  content_model: str) -> pywikibot.Page:
+        """Create a cached redirect page."""
+        page = pywikibot.Page(self.site, title)
+        page._pageid = 1
+        page._isredir = True
+        page._contentmodel = content_model
+        page._text = text
+        page._bot_may_edit = True
+        return page
+
+    def test_scribunto_redirect(self) -> None:
+        """Test setting a localized Scribunto redirect target."""
+        page = self._redirect(
+            'Modulo:SoundTable',
+            'return require [[Modulo:Sound table]]',
+            'Scribunto',
+        )
+        target = pywikibot.Page(
+            self.site, 'Modulo:TabellaSuoni#section')
+
+        page.set_redirect_target(target, keep_section=True, save=False)
+
+        self.assertEqual(
+            page.text, 'return require [[Modulo:TabellaSuoni]]')
+
+    def test_module_wikitext_redirect(self) -> None:
+        """Test that Module documentation keeps wikitext syntax."""
+        page = self._redirect(
+            'Modulo:SoundTable/doc',
+            '#REDIRECT [[Modulo:Sound table/doc]]',
+            'wikitext',
+        )
+        target = pywikibot.Page(self.site, 'Modulo:TabellaSuoni/doc')
+
+        page.set_redirect_target(target, save=False)
+
+        self.assertEqual(
+            page.text,
+            f'#{self.site.redirect()} [[Modulo:TabellaSuoni/doc]]',
+        )
+
+
 class TestPageBotMayEdit(TestCase):

     """Test Page.botMayEdit() method."""
diff --git a/tests/redirect_bot_tests.py b/tests/redirect_bot_tests.py
index 9edf82c..08e20fc 100755
--- a/tests/redirect_bot_tests.py
+++ b/tests/redirect_bot_tests.py
@@ -13,7 +13,7 @@

 import pywikibot
 from pywikibot import Page
-from scripts.redirect import RedirectRobot
+from scripts.redirect import RedirectGenerator, RedirectRobot
 from tests.aspects import DefaultSiteTestCase


@@ -32,6 +32,41 @@
         return pywikibot.Site()


+class TestRedirectGenerator(DefaultSiteTestCase):
+
+    """Test the RedirectGenerator class."""
+
+    dry = True
+
+    @patch('scripts.redirect.xmlreader.XmlDump')
+    def test_scribunto_redirect_from_dump(self, xml_dump) -> None:
+        """Test extracting a Scribunto redirect from an XML dump."""
+        entry = Mock(
+            title='SoundTable',
+            text='return require [[Sound table]]',
+            isredirect=True,
+            content_model='Scribunto',
+        )
+        not_redirect = Mock(
+            title='Lua example',
+            text='return require [[Example]]',
+            isredirect=False,
+            content_model='Scribunto',
+        )
+        xml_dump.return_value.parse.return_value = [entry, not_redirect]
+        with patch.object(pywikibot, 'Site', return_value=self.site):
+            generator = RedirectGenerator(
+                'double', xml='dump.xml', namespaces={0})
+
+        redirects, page_titles = generator.get_redirects_from_dump()
+
+        self.assertEqual(
+            redirects,
+            {'SoundTable': 'Sound_table'},
+        )
+        self.assertIsEmpty(page_titles)
+
+
 @patch.object(Page, 'exists', new=Mock(return_value=True))
 class TestGetSDTemplateNoSysop(DefaultSiteTestCase):

diff --git a/tests/xmlreader_tests.py b/tests/xmlreader_tests.py
index dfd7e63..dfa7f33 100755
--- a/tests/xmlreader_tests.py
+++ b/tests/xmlreader_tests.py
@@ -108,6 +108,7 @@
         for entry in entries:
             self.assertEqual(entry.username, 'Carlossuarez46')
             self.assertFalse(entry.isredirect)
+            self.assertEqual(entry.content_model, 'wikitext')

         articles = entries[0:2]
         talks = entries[2:4]

--
To view, visit 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1320232?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: I03e1afaac383b7e5715c33c5224a35343d607665
Gerrit-Change-Number: 1320232
Gerrit-PatchSet: 6
Gerrit-Owner: Mahveotm <[email protected]>
Gerrit-Reviewer: Ivan-r <[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