jenkins-bot has submitted this change. (
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/341342 )
Change subject: [IMPR] rewrite Revision class
......................................................................
[IMPR] rewrite Revision class
site.py:
- use a new method _rvprops to assign rvprop properties for
loadrevisions() and preloadpages(). Minimum supported
mw version is 1.19; remove other dependencies
- enable all rvprop properties
- rewrite loadrevisions
api.py:
- instantiate the Revision class with the revision dictionary.
All api results can be used and further api modifications would be much
easier to implement in the central Revision class
- page.title() is used to solve T102735 (content model for mw < 1.21)
page.py:
- move Revision class to _revision.py
_revision.py
- use a collection Mapping to hold the data. This disables changing
the revision.
- instantiate the Revision class with the revision dictionary via api.
- for backup compatibility cleanup positional args but deprecate
their usage; they shouldn't be used outside api.py
- upcast some data like timestamp
- take into account that slots are provided by some mw versions
- deprecte parent_id and content_model properties
- Remove private Revision._thank staticmethod which was used for thank tests
only but is not necessary to have.
thanks_tests.py
- use site.thank_revison method instead of Revision._thank
Bug: T102735
Bug: T259428
Change-Id: I6a87434aca7b83368a6fbf1742e1b10c7dbb3252
---
M pywikibot/CONTENT.rst
M pywikibot/data/api.py
M pywikibot/page/__init__.py
A pywikibot/page/_revision.py
M pywikibot/site/__init__.py
M tests/thanks_tests.py
6 files changed, 233 insertions(+), 203 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/CONTENT.rst b/pywikibot/CONTENT.rst
index 30f92ee..3446ee4 100644
--- a/pywikibot/CONTENT.rst
+++ b/pywikibot/CONTENT.rst
@@ -104,6 +104,8 @@
+============================+======================================================+
| __init__.py | Objects representing MediaWiki pages
|
+----------------------------+------------------------------------------------------+
+ | _revision.py | Object representing page revision
|
+
+----------------------------+------------------------------------------------------+
+----------------------------+------------------------------------------------------+
diff --git a/pywikibot/data/api.py b/pywikibot/data/api.py
index 81175d4..19f93d8 100644
--- a/pywikibot/data/api.py
+++ b/pywikibot/data/api.py
@@ -3172,23 +3172,19 @@
def _update_revisions(page, revisions):
"""Update page revisions."""
- # TODO: T102735: Use the page content model for <1.21
+ content_model = {'.js': 'javascript', '.css': 'css'}
for rev in revisions:
- revision = pywikibot.page.Revision(
- revid=rev['revid'],
- timestamp=pywikibot.Timestamp.fromISOformat(rev['timestamp']),
- user=rev.get('user', ''),
- anon='anon' in rev,
- comment=rev.get('comment', ''),
- minor='minor' in rev,
- slots=rev.get('slots'), # MW 1.32+
- text=rev.get('*'), # b/c
- rollbacktoken=rev.get('rollbacktoken'),
- parentid=rev.get('parentid'),
- contentmodel=rev.get('contentmodel'), # b/c
- sha1=rev.get('sha1')
- )
- page._revisions[revision.revid] = revision
+ if page.site.mw_version < '1.21':
+ # T102735: use content model depending on the page suffix
+ title = page.title(with_ns=False)
+ for suffix, cm in content_model.items():
+ if title.endswith(suffix):
+ rev['contentmodel'] = cm
+ break
+ else:
+ rev['contentmodel'] = 'wikitext'
+
+ page._revisions[rev['revid']] = pywikibot.page.Revision(**rev)
def _update_templates(page, templates):
diff --git a/pywikibot/page/__init__.py b/pywikibot/page/__init__.py
index 5452121..9f1a402 100644
--- a/pywikibot/page/__init__.py
+++ b/pywikibot/page/__init__.py
@@ -16,7 +16,6 @@
#
# Distributed under the terms of the MIT license.
#
-import hashlib
import logging
import os.path
import re
@@ -44,6 +43,7 @@
UserRightsError,
)
from pywikibot.family import Family
+from pywikibot.page._revision import Revision
from pywikibot.site import DataSite, Namespace, need_version
from pywikibot.tools import (
add_full_name,
@@ -5484,139 +5484,6 @@
}
-class Revision(DotReadableDict):
-
- """A structure holding information about a single revision of a Page."""
-
- def __init__(self, revid, timestamp, user, anon=False, comment='',
- text=None, minor=False, rollbacktoken=None, parentid=None,
- contentmodel=None, sha1=None, slots=None):
- """
- Initializer.
-
- All parameters correspond to object attributes (e.g., revid
- parameter is stored as self.revid)
-
- @param revid: Revision id number
- @type revid: int
- @param text: Revision wikitext.
- @type text: str, or None if text not yet retrieved
- @param timestamp: Revision time stamp
- @type timestamp: pywikibot.Timestamp
- @param user: user who edited this revision
- @type user: str
- @param anon: user is unregistered
- @type anon: bool
- @param comment: edit comment text
- @type comment: str
- @param minor: edit flagged as minor
- @type minor: bool
- @param rollbacktoken: rollback token
- @type rollbacktoken: str
- @param parentid: id of parent Revision
- @type parentid: int
- @param contentmodel: content model label (v1.21+)
- @type contentmodel: str
- @param sha1: sha1 of revision text
- @type sha1: str
- @param slots: revision slots (v1.32+)
- @type slots: dict
- """
- self.revid = revid
- self._text = text
- self.timestamp = timestamp
- self.user = user
- self.anon = anon
- self.comment = comment
- self.minor = minor
- self.rollbacktoken = rollbacktoken
- self._parent_id = parentid
- self._content_model = contentmodel
- self._sha1 = sha1
- self.slots = slots
-
- @property
- def parent_id(self) -> int:
- """
- Return id of parent/previous revision.
-
- Returns 0 if there is no previous revision
-
- @return: id of parent/previous revision
- @raises AssertionError: parent id not supplied to the constructor
- """
- assert self._parent_id is not None, (
- 'Revision {0} was instantiated without a parent id'
- .format(self.revid))
-
- return self._parent_id
-
- @property
- def text(self) -> Optional[str]:
- """
- Return text of this revision.
-
- This is meant for compatibility with older MW version which
- didn't support revisions with slots. For newer MW versions,
- this returns the contents of the main slot.
-
- @return: text of the revision
- """
- if self.slots is not None:
- return self.slots.get('main', {}).get('*')
- return self._text
-
- @property
- def content_model(self) -> str:
- """
- Return content model of the revision.
-
- This is meant for compatibility with older MW version which
- didn't support revisions with slots. For newer MW versions,
- this returns the content model of the main slot.
-
- @return: content model
- @raises AssertionError: content model not supplied to the constructor
- which always occurs for MediaWiki versions lower than 1.21.
- """
- if self._content_model is None and self.slots is not None:
- self._content_model = self.slots.get('main', {}).get(
- 'contentmodel')
- # TODO: T102735: Add a sane default of 'wikitext' and others for <1.21
- assert self._content_model is not None, (
- 'Revision {0} was instantiated without a content model'
- .format(self.revid))
-
- return self._content_model
-
- @property
- def sha1(self) -> Optional[str]:
- """
- Return and cache SHA1 checksum of the text.
-
- @return: if the SHA1 checksum is cached it'll be returned which is the
- case when it was requested from the API. Otherwise it'll use the
- revision's text to calculate the checksum (encoding it using UTF8
- first). That calculated checksum will be cached too and returned on
- future calls. If the text is None (not queried) it will just return
- None and does not cache anything.
- """
- if self._sha1 is None and self.text is not None:
- self._sha1 = hashlib.sha1(self.text.encode('utf8')).hexdigest()
- return self._sha1
-
- @staticmethod
- def _thank(revid, site, source='pywikibot'):
- """Thank a user for this revision.
-
- @param site: The Site object for this revision.
- @type site: Site
- @param source: An optional source to pass to the API.
- @type source: str
- """
- site.thank_revision(revid, source)
-
-
class FileInfo(DotReadableDict):
"""
diff --git a/pywikibot/page/_revision.py b/pywikibot/page/_revision.py
new file mode 100644
index 0000000..9331b46
--- /dev/null
+++ b/pywikibot/page/_revision.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+"""Object representing page revision."""
+#
+# (C) Pywikibot team, 2008-2020
+#
+# Distributed under the terms of the MIT license.
+#
+import hashlib
+
+from collections.abc import Mapping
+
+from pywikibot import Timestamp, warning
+from pywikibot.tools import deprecated, issue_deprecation_warning
+
+
+class Revision(Mapping):
+
+ """A structure holding information about a single revision of a Page.
+
+ Each data item can be accessed either by its key or as an attribute
+ with the attribute name equal to the key e.g.:
+
+ >>> r = Revision(comment='Sample for Revision access')
+ >>> r.comment == r['comment']
+ True
+ >>> r.comment
+ Sample for Revision access
+ """
+
+ def __init__(self, *args, **kwargs):
+ """Initializer."""
+ self._clean_args(args, kwargs)
+ self._data = kwargs
+ self._upcast_dict(self._data)
+ super().__init__()
+
+ def _clean_args(self, args: tuple, kwargs: dict):
+ """Cleanup positional arguments.
+
+ Replace positional arguments with keyword arguments for
+ backwards compatibility.
+ @param: args: tuple of positional arguments
+ @param: kwargs: mutable dict of keyword arguments to be updated
+ """
+ keys = ( # postional argument keys by old order
+ 'revid', 'timestamp', 'user', 'anon', 'comment', 'text', 'minor',
+ 'rollbacktoken', 'parentid', 'contentmodel', 'sha1', 'slots'
+ )
+
+ # replace positional arguments with keyword arguments
+ for i, (arg, key) in enumerate(zip(args, keys)):
+ issue_deprecation_warning('Positional argument {} ({})'
+ .format(i + 1, arg),
+ 'keyword argument "{}={}"'
+ .format(key, arg),
+ warning_class=FutureWarning,
+ since='20200802')
+ if key in kwargs:
+ warning('"{}" is given as keyword argument "{}" already; '
+ 'ignoring "{}"'.format(key, arg, kwargs[key]))
+ else:
+ kwargs[key] = arg
+
+ @staticmethod
+ def _upcast_dict(map_):
+ """Upcast dictionary values."""
+ map_['timestamp'] = Timestamp.fromISOformat(map_['timestamp'])
+
+ map_.update(anon='anon' in map_)
+ map_.setdefault('comment', '')
+ map_.update(minor='minor' in map_)
+
+ if 'slots' in map_: # mw 1.32+
+ mainslot = map_['slots'].get('main', {})
+ map_['text'] = mainslot.get('*')
+ map_['contentmodel'] = mainslot.get('contentmodel')
+ else:
+ map_['slots'] = None
+ map_['text'] = map_.get('*')
+
+ map_.setdefault('sha1')
+ if map_['sha1'] is None and map_['text'] is not None:
+ map_['sha1'] = hashlib.sha1(
+ map_['text'].encode('utf8')).hexdigest()
+
+ def __len__(self) -> int:
+ """Return the number of data items."""
+ return len(self._data)
+
+ def __getitem__(self, name: str):
+ """Return a single Revision item given by name."""
+ if name in self._data:
+ return self._data[name]
+ if name in ('parent_id', 'content_model'):
+ return getattr(self, name)
+
+ return self.__missing__(name)
+
+ # provide attribute access
+ __getattr__ = __getitem__
+
+ def __iter__(self):
+ """Provide Revision data as iterator."""
+ return iter(self._data)
+
+ def __repr__(self):
+ """String representation of Revision."""
+ return '{}({})'.format(self.__class__.__name__, self._data)
+
+ def __str__(self):
+ """Printable representation of Revision data."""
+ return str(self._data)
+
+ def __missing__(self, key):
+ """Provide backward compatibility for exceptions."""
+ if key == 'parentid':
+ raise AssertionError(
+ 'Revision {rev.revid} was instantiated without a parent id'
+ .format(rev=self))
+
+ if key == 'rollbacktoken':
+ warning('"rollbacktoken" has been deprecated since MediaWiki 1.24')
+ return None
+
+ # raise AttributeError instead of KeyError for backward compatibility
+ raise AttributeError("'{}' object has no attribute '{}'"
+ .format(self.__class__.__name__, key))
+
+ @property
+ @deprecated('parentid property', since='20200802')
+ def parent_id(self) -> int:
+ """DEPRECATED. Return id of parent/previous revision.
+
+ Returns 0 if there is no previous revision
+
+ @return: id of parent/previous revision
+ @raises AssertionError: parent id not supplied to the constructor
+ """
+ return self.parentid
+
+ @property
+ @deprecated('contentmodel', since='20200802')
+ def content_model(self) -> str:
+ """DEPRECATED. Return content model of the revision.
+
+ This is meant for compatibility with older MW version which
+ didn't support revisions with slots. For newer MW versions,
+ this returns the content model of the main slot.
+
+ @return: content model
+ @raises AssertionError: content model not supplied to the constructor
+ which always occurs for MediaWiki versions lower than 1.21.
+ """
+ return self.contentmodel
diff --git a/pywikibot/site/__init__.py b/pywikibot/site/__init__.py
index edfbf11..0ce5158 100644
--- a/pywikibot/site/__init__.py
+++ b/pywikibot/site/__init__.py
@@ -3089,8 +3089,6 @@
if pageprops:
props += '|pageprops'
- rvprop = ['ids', 'flags', 'timestamp', 'user', 'comment', 'content']
-
parameter = self._paraminfo.parameter('query+info', 'prop')
if self.logged_in() and self.has_right('apihighlimits'):
max_ids = int(parameter['highlimit'])
@@ -3120,7 +3118,7 @@
rvgen.request['pageids'] = set(pageids)
else:
rvgen.request['titles'] = list(cache.keys())
- rvgen.request['rvprop'] = rvprop
+ rvgen.request['rvprop'] = self._rvprops(content=True)
pywikibot.output('Retrieving %s pages from %s.'
% (len(cache), self))
@@ -3724,12 +3722,24 @@
yield from self._generator(api.PageGenerator, namespaces=namespaces,
total=total, g_content=content, **cmargs)
+ def _rvprops(self, content=False) -> list:
+ """Setup rvprop items for loadrevisions and preloadpages.
+
+ @return: rvprop items
+ """
+ props = ['comment', 'ids', 'flags', 'parsedcomment', 'sha1', 'size',
+ 'tags', 'timestamp', 'user', 'userid']
+ if content:
+ props.append('content')
+ if self.mw_version >= '1.21':
+ props.append('contentmodel')
+ if self.mw_version >= '1.32':
+ props.append('roles')
+ return props
+
@deprecated_args(getText='content', sysop=None)
@remove_last_args(['rollback'])
- def loadrevisions(self, page, *, content=False, revids=None,
- startid=None, endid=None, starttime=None,
- endtime=None, rvdir=None, user=None, excludeuser=None,
- section=None, step=None, total=None):
+ def loadrevisions(self, page, *, content=False, section=None, **kwargs):
"""Retrieve revision information and store it in page object.
By default, retrieves the last (current) revision of the page,
@@ -3753,61 +3763,60 @@
(content must be True); section must be given by number (top of
the article is section 0), not name
@type section: int
- @param revids: retrieve only the specified revision ids (raise
+ @keyword revids: retrieve only the specified revision ids (raise
Exception if any of revids does not correspond to page)
@type revids: an int, a str or a list of ints or strings
- @param startid: retrieve revisions starting with this revid
- @param endid: stop upon retrieving this revid
- @param starttime: retrieve revisions starting at this Timestamp
- @param endtime: stop upon reaching this Timestamp
- @param rvdir: if false, retrieve newest revisions first (default);
+ @keyword startid: retrieve revisions starting with this revid
+ @keyword endid: stop upon retrieving this revid
+ @keyword starttime: retrieve revisions starting at this Timestamp
+ @keyword endtime: stop upon reaching this Timestamp
+ @keyword rvdir: if false, retrieve newest revisions first (default);
if true, retrieve earliest first
- @param user: retrieve only revisions authored by this user
- @param excludeuser: retrieve all revisions not authored by this user
+ @keyword user: retrieve only revisions authored by this user
+ @keyword excludeuser: retrieve all revisions not authored by this user
@raises ValueError: invalid startid/endid or starttime/endtime values
@raises pywikibot.Error: revids belonging to a different page
"""
- latest = (revids is None
- and startid is None
- and endid is None
- and starttime is None
- and endtime is None
- and rvdir is None
- and user is None
- and excludeuser is None
- and step is None
- and total is None) # if True, retrieving current revision
+ latest = all(val is None for val in kwargs.values())
+
+ revids = kwargs.get('revids')
+ startid = kwargs.get('startid')
+ starttime = kwargs.get('starttime')
+ endid = kwargs.get('endid')
+ endtime = kwargs.get('endtime')
+ rvdir = kwargs.get('rvdir')
+ user = kwargs.get('user')
+ step = kwargs.get('step')
# check for invalid argument combinations
- if (startid is not None or endid is not None) and \
- (starttime is not None or endtime is not None):
+ if (startid is not None or endid is not None) \
+ and (starttime is not None or endtime is not None):
raise ValueError(
'loadrevisions: startid/endid combined with starttime/endtime')
+
if starttime is not None and endtime is not None:
if rvdir and starttime >= endtime:
raise ValueError(
'loadrevisions: starttime > endtime with rvdir=True')
- if (not rvdir) and endtime >= starttime:
+
+ if not rvdir and endtime >= starttime:
raise ValueError(
'loadrevisions: endtime > starttime with rvdir=False')
+
if startid is not None and endid is not None:
if rvdir and startid >= endid:
raise ValueError(
'loadrevisions: startid > endid with rvdir=True')
- if (not rvdir) and endid >= startid:
+ if not rvdir and endid >= startid:
raise ValueError(
'loadrevisions: endid > startid with rvdir=False')
rvargs = {'type_arg': 'info|revisions'}
+ rvargs['rvprop'] = self._rvprops(content=content)
- rvargs['rvprop'] = ['comment', 'flags', 'ids', 'sha1', 'timestamp',
- 'user']
- if self.mw_version >= '1.21':
- rvargs['rvprop'].append('contentmodel')
- if content:
- rvargs['rvprop'].append('content')
- if section is not None:
- rvargs['rvsection'] = str(section)
+ if content and section is not None:
+ rvargs['rvsection'] = str(section)
+
if revids is None:
rvtitle = page.title(with_section=False).encode(self.encoding())
rvargs['titles'] = rvtitle
@@ -3822,6 +3831,7 @@
rvargs['rvdir'] = 'newer'
elif rvdir is not None:
rvargs['rvdir'] = 'older'
+
if startid:
rvargs['rvstartid'] = startid
if endid:
@@ -3830,13 +3840,16 @@
rvargs['rvstart'] = starttime
if endtime:
rvargs['rvend'] = endtime
+
if user:
rvargs['rvuser'] = user
- elif excludeuser:
- rvargs['rvexcludeuser'] = excludeuser
+ else:
+ rvargs['rvexcludeuser'] = kwargs.get('excludeuser')
# assemble API request
- rvgen = self._generator(api.PropertyGenerator, total=total, **rvargs)
+ rvgen = self._generator(api.PropertyGenerator,
+ total=kwargs.get('total'), **rvargs)
+
if step:
rvgen.set_query_increment = step
diff --git a/tests/thanks_tests.py b/tests/thanks_tests.py
index e005c82..5c10243 100644
--- a/tests/thanks_tests.py
+++ b/tests/thanks_tests.py
@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*-
"""Tests for thanks-related code."""
#
-# (C) Pywikibot team, 2016-2019
+# (C) Pywikibot team, 2016-2020
#
# Distributed under the terms of the MIT license.
#
-from __future__ import absolute_import, division, unicode_literals
+from contextlib import suppress
-from pywikibot.page import Page, Revision, User
+from pywikibot.page import Page, User
from tests.aspects import TestCase
from tests import unittest
@@ -43,7 +43,7 @@
else:
self.skipTest(NO_THANKABLE_REVS)
before_time = site.getcurrenttimestamp()
- Revision._thank(revid, site, source='pywikibot test')
+ site.thank_revision(revid, source='pywikibot test')
log_entries = site.logevents(logtype='thanks', total=5, page=user,
start=before_time, reverse=True)
try:
@@ -72,8 +72,8 @@
test_page.text += '* ~~~~\n'
test_page.save('Pywikibot Thanks test')
revid = test_page.latest_revision_id
- self.assertAPIError('invalidrecipient', None, Revision._thank,
- revid, site, source='pywikibot test')
+ self.assertAPIError('invalidrecipient', None, site.thank_revision,
+ revid, source='pywikibot test')
class TestThankRevisionErrors(TestCase):
@@ -97,8 +97,8 @@
break
else:
self.skipTest(NO_THANKABLE_REVS)
- self.assertAPIError('invalidrecipient', None, Revision._thank,
- revid, site, source='pywikibot test')
+ self.assertAPIError('invalidrecipient', None, site.thank_revision,
+ revid, source='pywikibot test')
def test_invalid_revision(self):
"""Test that passing an invalid revision ID causes an error."""
@@ -106,12 +106,10 @@
invalid_revids = (0, -1, 0.99, 'zero, minus one, and point nine nine',
(0, -1, 0.99), [0, -1, 0.99])
for invalid_revid in invalid_revids:
- self.assertAPIError('invalidrevision', None, Revision._thank,
- invalid_revid, site, source='pywikibot test')
+ self.assertAPIError('invalidrevision', None, site.thank_revision,
+ invalid_revid, source='pywikibot test')
if __name__ == '__main__': # pragma: no cover
- try:
+ with suppress(SystemExit):
unittest.main()
- except SystemExit:
- pass
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/341342
To unsubscribe, or for help writing mail filters, visit
https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I6a87434aca7b83368a6fbf1742e1b10c7dbb3252
Gerrit-Change-Number: 341342
Gerrit-PatchSet: 13
Gerrit-Owner: Xqt <[email protected]>
Gerrit-Reviewer: Dalba <[email protected]>
Gerrit-Reviewer: John Vandenberg <[email protected]>
Gerrit-Reviewer: Magul <[email protected]>
Gerrit-Reviewer: Matěj Suchánek <[email protected]>
Gerrit-Reviewer: Mpaa <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
_______________________________________________
Pywikibot-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/pywikibot-commits