jenkins-bot has submitted this change. (
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1185306?usp=email )
Change subject: IMPR: refactor Site.rollbackpage
......................................................................
IMPR: refactor Site.rollbackpage
- Add *pageid* parameter as alternative to *page* and add checks for them
- Set defaults for *markbot* if not explicitly given
- The method now returns a dictionary with rollback information
- No longer search the history for different users, let the API check it
- Raise NoPageError of pageid does not exists
- Modify PageRelatedError to enable exception for pageid
- Add rollback() method to BasePage
- Use BasePage.rollback in BaseRevertBot
- Modify API result when simulate is set
- Add TestRollbackPage to site_tests
- Update documentation
Bug: T403425
Change-Id: Ibb559d810e44a52fcf1167b8d2d7f07de374fecc
---
M docs/mwapi.rst
M pywikibot/data/api/_requests.py
M pywikibot/exceptions.py
M pywikibot/page/_basepage.py
M pywikibot/site/_apisite.py
M scripts/pyproject.toml
M scripts/revertbot.py
M tests/site_tests.py
8 files changed, 246 insertions(+), 53 deletions(-)
Approvals:
jenkins-bot: Verified
Matěj Suchánek: Looks good to me, approved
diff --git a/docs/mwapi.rst b/docs/mwapi.rst
index f8a9748..10a90e0 100644
--- a/docs/mwapi.rst
+++ b/docs/mwapi.rst
@@ -103,7 +103,7 @@
-
* - :api:`rollback<rollback>`
- :meth:`rollbackpage()<pywikibot.site._apisite.APISite.rollbackpage>`
- -
+ - meth:`BasePage.rollback()<page.BasePage.rollback>`
-
* - :api:`shortenurl<shortenurl>`
-
:meth:`create_short_link()<pywikibot.site._extensions.UrlShortenerMixin.create_short_link>`
diff --git a/pywikibot/data/api/_requests.py b/pywikibot/data/api/_requests.py
index 07fa07f..a49bc53 100644
--- a/pywikibot/data/api/_requests.py
+++ b/pywikibot/data/api/_requests.py
@@ -532,8 +532,16 @@
# for more realistic simulation
if config.simulate is not True:
pywikibot.sleep(float(config.simulate))
+ if action == 'rollback':
+ result = {
+ 'title': self._params['title'][0].title(),
+ 'summary': self._params.get('summary',
+ ['Rollback simulation'])[0],
+ }
+ else:
+ result = {'result': 'Success', 'nochange': ''}
return {
- action: {'result': 'Success', 'nochange': ''},
+ action: result,
# wikibase results
'entity': {'lastrevid': -1, 'id': '-1'},
diff --git a/pywikibot/exceptions.py b/pywikibot/exceptions.py
index 66ae9b0..00f1a26 100644
--- a/pywikibot/exceptions.py
+++ b/pywikibot/exceptions.py
@@ -172,7 +172,7 @@
instead.
"""
#
-# (C) Pywikibot team, 2008-2023
+# (C) Pywikibot team, 2008-2025
#
# Distributed under the terms of the MIT license.
#
@@ -286,17 +286,20 @@
This class should be used when the Exception concerns a particular
Page, and when a generic message can be written once for all.
+
+ .. versionchanged:: 10.5
+ A pageid is accepted with the first parameter
"""
# Preformatted message where the page title will be inserted.
# Override this in subclasses.
message = ''
- def __init__(self, page: pywikibot.page.BasePage,
+ def __init__(self, page: pywikibot.page.BasePage | int,
message: str | None = None) -> None:
"""Initializer.
- :param page: Page that caused the exception
+ :param page: Page object or pageid that caused the exception
"""
if message:
self.message = message
@@ -305,13 +308,17 @@
raise Error("PageRelatedError is abstract. Can't instantiate it!")
self.page = page
- self.title = page.title(as_link=True)
- self.site = page.site
+ if isinstance(page, pywikibot.page.BasePage):
+ self.title = str(page)
+ self.site = page.site
+ else:
+ self.title = f'{page} (pageid)'
+ self.site = ''
if re.search(r'\{\w+\}', self.message):
msg = self.message.format_map(self.__dict__)
else:
- msg = self.message.format(page)
+ msg = self.message.format(self.title)
super().__init__(msg)
diff --git a/pywikibot/page/_basepage.py b/pywikibot/page/_basepage.py
index 1e7ca3f..f10c1d1 100644
--- a/pywikibot/page/_basepage.py
+++ b/pywikibot/page/_basepage.py
@@ -2004,6 +2004,50 @@
noredirect=noredirect,
movesubpages=movesubpages)
+ def rollback(self, **kwargs: Any) -> dict[str, int | str]:
+ """Roll back this page to the version before the last edit by a user.
+
+ .. versionadded:: 10.5
+
+ .. seealso::
+ :meth:`Site.rollbackpage()
+ <pywikibot.site._apisite.APISite.rollbackpage>`
+
+ :keyword tags: Tags to apply to the rollback.
+ :kwtype tags: str | Sequence[str] | None
+ :keyword str user: The last user to be rolled back; Default is
+ :attr:`BasePage.latest_revision.user
+ <page.BasePage.latest_revision>`.
+ :keyword str | None summary: Custom edit summary for the rollback
+ :keyword bool | None markbot: Mark the reverted edits and the
+ revert as bot edits. If not given, it is set to True if the
+ rollback user belongs to the 'bot' group, otherwise False.
+ :keyword watchlist: Unconditionally add or remove the page from
+ the current user's watchlist; 'preferences' is ignored for
+ bot users.
+ :kwtype watchlist: Literal['watch', 'unwatch', 'preferences',
+ 'nochange'] | None
+ :keyword watchlistexpiry: Watchlist expiry timestamp. Omit this
+ parameter entirely to leave the current expiry unchanged.
+ :kwtype watchlistexpiry: pywikibot.Timestamp | str | Literal[
+ 'infinite', 'indefinite', 'infinity', 'never'] | None
+ :returns: Dictionary containing rollback result like
+
+ .. code:: python
+
+ {
+ 'title': <page title>,
+ 'pageid': <page ID>,
+ 'summary': <rollback summary>,
+ 'revid': <ID of the new revision created by the rollback>,
+ 'old_revid': <ID of the newest revision being rolled back>,
+ 'last_revid': <ID of the revision restored by the rollback>,
+ }
+
+ raises exceptions.Error: The rollback fails.
+ """
+ return self.site.rollbackpage(self, **kwargs)
+
def delete(
self,
reason: str | None = None,
diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py
index c52c2ba..c4f2764 100644
--- a/pywikibot/site/_apisite.py
+++ b/pywikibot/site/_apisite.py
@@ -2505,68 +2505,122 @@
# catalog of rollback errors for use in error messages
_rb_errors = {
- 'noapiwrite': 'API editing not enabled on {site} wiki',
- 'writeapidenied': 'User {user} not allowed to edit through the API',
- 'alreadyrolled':
- 'Page [[{title}]] already rolled back; action aborted.',
- } # other errors shouldn't arise because we check for those errors
+ 'alreadyrolled': 'The last edit of page {title!r} by user {user!r} '
+ 'was already rolled back.',
+ 'onlyauthor': 'The page {title!r} has only {user!r} as author',
+ } # standard error messages raises API error
@need_right('rollback')
def rollbackpage(
self,
- page: BasePage,
+ page: BasePage | None = None,
+ *,
+ pageid: int | None = None,
**kwargs: Any
- ) -> None:
- """Roll back page to version before last user's edits.
+ ) -> dict[str, int | str]:
+ """Roll back a page to the version before the last edit by a user.
- .. seealso:: :api:`Rollback`
+ This method wraps the MediaWiki :api:`Rollback`. The rollback
+ will revert the last edit(s) made by the specified user on the
+ given page.
- The keyword arguments are those supported by the rollback API.
+ .. versionchanged:: 10.5
+ Added *pageid* as alternative to *page* (one must be given).
+ *markbot* defaults to True if the rollbacker is a bot and not
+ explicitly given. The method now returns a dictionary with
+ rollback information.
- As a precaution against errors, this method will fail unless
- the page history contains at least two revisions, and at least
- one that is not by the same user who made the last edit.
+ .. seealso::
+ :meth:`page.BasePage.rollback`
- :param page: the Page to be rolled back (must exist)
- :keyword user: the last user to be rollbacked;
- default is page.latest_revision.user
+ :param page: the Page to be rolled back. Cannot be used together
+ with *pageid*.
+ :param pageid: Page ID of the page to be rolled back. Cannot be
+ used together with *page*.
+ :keyword tags: Tags to apply to the rollback.
+ :kwtype tags: str | Sequence[str] | None
+ :keyword str user: The last user to be rolled back; Must be
+ given with *pageid*. Default is
+ :attr:`BasePage.latest_revision.user
+ <page.BasePage.latest_revision>` if *page* is given.
+ :keyword str | None summary: Custom edit summary for the rollback
+ :keyword bool | None markbot: Mark the reverted edits and the
+ revert as bot edits. If not given, it is set to True if the
+ rollback user belongs to the 'bot' group, otherwise False.
+ :keyword watchlist: Unconditionally add or remove the page from
+ the current user's watchlist; 'preferences' is ignored for
+ bot users.
+ :kwtype watchlist: Literal['watch', 'unwatch', 'preferences',
+ 'nochange'] | None
+ :keyword watchlistexpiry: Watchlist expiry timestamp. Omit this
+ parameter entirely to leave the current expiry unchanged.
+ :kwtype watchlistexpiry: pywikibot.Timestamp | str | Literal[
+ 'infinite', 'indefinite', 'infinity', 'never'] | None
+ :returns: Dictionary containing rollback result like
+
+ .. code:: python
+
+ {
+ 'title': <page title>,
+ 'pageid': <page ID>,
+ 'summary': <rollback summary>,
+ 'revid': <ID of the new revision created by the rollback>,
+ 'old_revid': <ID of the newest revision being rolled back>,
+ 'last_revid': <ID of the revision restored by the rollback>,
+ }
+
+ :raises APIError: An error was returned by the rollback API, or
+ another standard API error occurred.
+ :raises Error: The page was already rolled back, or the given
+ *user* is the only author.
+ :raises NoPageError: The given *page* or *pageid* does not exist.
+ :raises TypeError: *pageid* is of invalid type.
+ :raises ValueError: Both *page* and *pageid* were given, or none
+ of them, or *pageid* has an invalid value.
"""
- if len(page._revisions) < 2:
- raise Error(
- f'Rollback of {page} aborted; load revision history first.')
+ if page is not None and pageid is not None:
+ raise ValueError(
+ "The parameters 'page' and 'pageid' cannot be used together.")
+
+ if page is None and pageid is None:
+ raise ValueError(
+ "One of parameters 'page' or 'pageid' is required.")
+
+ if page is None and pageid is not None:
+ page = next(self.load_pages_from_pageids(str(pageid)), None)
+
+ if page is None:
+ raise NoPageError(pageid)
user = kwargs.pop('user', page.latest_revision.user)
- for rev in sorted(page._revisions.values(), reverse=True,
- key=lambda r: r.timestamp):
- # start with most recent revision first
- if rev.user != user:
- break
- else:
- raise Error(f'Rollback of {page} aborted; only one user in '
- f'revision history.')
+ params = merge_unique_dicts(
+ kwargs,
+ action='rollback',
+ title=page,
+ token=self.tokens['rollback'],
+ user=user,
+ )
- parameters = merge_unique_dicts(kwargs,
- action='rollback',
- title=page,
- token=self.tokens['rollback'],
- user=user)
+ rb_user = self.user()
+ if rb_user is not None and 'markbot' not in kwargs:
+ params['markbot'] = self.has_group('bot')
+
self.lock_page(page)
- req = self.simple_request(**parameters)
+ req = self.simple_request(**params)
try:
- req.submit()
+ result = req.submit()
except APIError as err:
errdata = {
- 'site': self,
'title': page.title(with_section=False),
- 'user': self.user(),
+ 'user': user,
}
if err.code in self._rb_errors:
raise Error(
self._rb_errors[err.code].format_map(errdata)
) from None
- pywikibot.debug(
- f"rollback: Unexpected error code '{err.code}' received.")
raise
+ else:
+ return result['rollback']
finally:
self.unlock_page(page)
diff --git a/scripts/pyproject.toml b/scripts/pyproject.toml
index 0886321..72811b7 100644
--- a/scripts/pyproject.toml
+++ b/scripts/pyproject.toml
@@ -19,7 +19,7 @@
readme = "scripts/README.rst"
requires-python = ">=3.8.0"
dependencies = [
- "pywikibot >= 10.4.0",
+ "pywikibot >= 10.5.0",
"isbnlib",
"langdetect",
"mwparserfromhell",
diff --git a/scripts/revertbot.py b/scripts/revertbot.py
index 64fe498..015871b 100755
--- a/scripts/revertbot.py
+++ b/scripts/revertbot.py
@@ -36,12 +36,14 @@
return False
"""
#
-# (C) Pywikibot team, 2008-2024
+# (C) Pywikibot team, 2008-2025
#
# Distributed under the terms of the MIT license.
#
from __future__ import annotations
+from textwrap import fill
+
import pywikibot
from pywikibot import i18n
from pywikibot.backports import Container
@@ -84,7 +86,8 @@
if callback(item):
result = self.revert(item)
if result:
- pywikibot.info(f"{item['title']}: {result}")
+ pywikibot.info(
+ fill(f"{item['title']}: {result}", width=77))
else:
pywikibot.info(f"Skipped {item['title']}")
else:
@@ -134,17 +137,17 @@
return comment
try:
- self.site.rollbackpage(page, user=self.user, markbot=True)
+ result = page.rollback(user=self.user)
except APIError as e:
if e.code == 'badtoken':
pywikibot.error(
- 'There was an API token error rollbacking the edit')
+ 'There was an API token error rolling back the edit')
return False
except Error:
pass
else:
- return (f'The edit(s) made in {page.title()} by {self.user}'
- ' was rollbacked')
+ return (f'The edit(s) made in {result["title"]} by {self.user} '
+ f'was rolled back to revision {result["last_revid"]}')
pywikibot.exception(exc_info=False)
return False
diff --git a/tests/site_tests.py b/tests/site_tests.py
index 8c3df54..ba06e32 100755
--- a/tests/site_tests.py
+++ b/tests/site_tests.py
@@ -31,6 +31,7 @@
DefaultDrySiteTestCase,
DefaultSiteTestCase,
DeprecationTestCase,
+ PatchingTestCase,
TestCase,
WikimediaDefaultSiteTestCase,
)
@@ -784,6 +785,82 @@
site.undelete(fp, 'pywikibot unit tests', fileids=[fileid])
+class TestRollbackPage(PatchingTestCase):
+
+ """Test rollbackpage site method."""
+
+ family = 'wikipedia'
+ code = 'test'
+ login = True
+
+ @staticmethod
+ @PatchingTestCase.patched(pywikibot.data.api.Request, '_simulate')
+ def _simulate(self, action):
+ """Patch api.Request._simulate. Note: self is the Request instance."""
+ if action == 'rollback':
+ result = {
+ 'title': self._params['title'][0].title(),
+ 'summary': self._params.get('summary',
+ ['Rollback simulation'])[0],
+ 'last_revid': 381070,
+ }
+ return {action: result}
+
+ if action and config.simulate and self.write:
+ result = {'result': 'Success', 'nochange': ''}
+ return {action: result}
+
+ return None
+
+ @classmethod
+ def setUpClass(cls):
+ """Use sandbox page for tests."""
+ super().setUpClass()
+ cls.page = pywikibot.Page(cls.site, 'Sandbox')
+
+ def setUp(self):
+ """Patch has_right method."""
+ super().setUp()
+ self.patch(self.site, 'has_right', lambda right: True)
+
+ def test_missing_rights(self):
+ """Test missing rollback right."""
+ self.patch(self.site, 'has_right', lambda right: False)
+ with self.assertRaisesRegex(
+ Error,
+ r'User "\w+" does not have required user right "rollback" on site'
+ ):
+ self.site.rollbackpage(self.page, pageid=4711)
+
+ def test_exceptions(self):
+ """Test rollback exceptions."""
+ with self.assertRaisesRegex(
+ ValueError,
+ "The parameters 'page' and 'pageid' cannot be used together"
+ ):
+ self.site.rollbackpage(self.page, pageid=4711)
+
+ with self.assertRaisesRegex(
+ ValueError,
+ r"One of parameters 'page' or 'pageid' is required\."
+ ):
+ self.site.rollbackpage()
+
+ with self.assertRaisesRegex(
+ NoPageError, r"Page -1 \(pageid\) doesn't exist\."):
+ self.site.rollbackpage(pageid=-1)
+
+ def test_rollback_simulation(self):
+ """Test rollback in simulate mode."""
+ result = self.site.rollbackpage(self.page)
+ self.assertIsInstance(result, dict)
+ self.assertEqual(result['title'], self.page.title())
+ self.assertEqual(result['last_revid'], 381070)
+ self.assertEqual(result['summary'], 'Rollback simulation')
+ result = self.site.rollbackpage(self.page, summary='Rollback test')
+ self.assertEqual(result['summary'], 'Rollback test')
+
+
class TestUsernameInUsers(DefaultSiteTestCase):
"""Test that the user account can be found in users list."""
--
To view, visit
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1185306?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: Ibb559d810e44a52fcf1167b8d2d7f07de374fecc
Gerrit-Change-Number: 1185306
Gerrit-PatchSet: 11
Gerrit-Owner: Xqt <[email protected]>
Gerrit-Reviewer: Matěj Suchánek <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]