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]