jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1311863?usp=email )
Change subject: IMPR: Add a new collection class DataRecord an derive Revision from it ......................................................................
IMPR: Add a new collection class DataRecord an derive Revision from it
Bug: T432464 Change-Id: I8f2e6f04ebd733aad333190df7cbc4f182cef62a --- M docs/conf.py M pywikibot/page/_revision.py M pywikibot/tools/collections.py 3 files changed, 99 insertions(+), 57 deletions(-)
Approvals: Xqt: Looks good to me, approved jenkins-bot: Verified
diff --git a/docs/conf.py b/docs/conf.py index dc02ef8..e113e7c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -668,6 +668,6 @@ autodoc_default_options = { 'members': True, 'undoc-members': True, - 'special-members': False, + 'special-members': '__missing__', 'show-inheritance': True, } diff --git a/pywikibot/page/_revision.py b/pywikibot/page/_revision.py index 81f093d..dc4a724 100644 --- a/pywikibot/page/_revision.py +++ b/pywikibot/page/_revision.py @@ -7,13 +7,17 @@ from __future__ import annotations
import hashlib -from collections.abc import Mapping from contextlib import suppress +from typing import Any
from pywikibot import Timestamp +from pywikibot.tools.collections import DataRecord
-class Revision(Mapping): +__all__ = ('Revision', ) + + +class Revision(DataRecord):
"""A structure holding information about a single revision of a Page.
@@ -32,68 +36,35 @@ - :api:`Alldeletedrevisions` """
- def __init__(self, **kwargs) -> None: - """Initializer.""" - self._data = kwargs - self._upcast_dict(self._data) - super().__init__() - @staticmethod - def _upcast_dict(map_) -> None: + def normalize(data: dict[str, Any]) -> None: """Upcast dictionary values.""" with suppress(KeyError): # enable doctest - map_['timestamp'] = Timestamp.fromISOformat(map_['timestamp']) + data['timestamp'] = Timestamp.fromISOformat(data['timestamp'])
- map_.update(anon='anon' in map_) - map_.update(minor='minor' in map_) - map_.update(userhidden='userhidden' in map_) - map_.update(commenthidden='commenthidden' in map_) + data.update(anon='anon' in data) + data.update(minor='minor' in data) + data.update(userhidden='userhidden' in data) + data.update(commenthidden='commenthidden' in data)
- map_.setdefault('comment', '') - map_.setdefault('user', '') + data.setdefault('comment', '') + data.setdefault('user', '')
- if 'slots' in map_: # mw 1.32+ - mainslot = map_['slots'].get('main', {}) - map_['text'] = mainslot.get('*') - map_['contentmodel'] = mainslot.get('contentmodel') + if 'slots' in data: # mw 1.32+ + mainslot = data['slots'].get('main', {}) + data['text'] = mainslot.get('*') + data['contentmodel'] = mainslot.get('contentmodel') else: - map_['slots'] = None - map_['text'] = map_.get('*') + data['slots'] = None + data['text'] = data.get('*')
- map_.setdefault('sha1') - if map_['sha1'] is None and map_['text'] is not None: - map_['sha1'] = hashlib.sha1( - map_['text'].encode('utf8')).hexdigest() + data.setdefault('sha1') + if data['sha1'] is None and data['text'] is not None: + data['sha1'] = hashlib.sha1( + data['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] - - return self.__missing__(name) - - # provide attribute access - __getattr__ = __getitem__ - - def __iter__(self): - """Provide Revision data as iterator.""" - return iter(self._data) - - def __repr__(self) -> str: - """String representation of Revision.""" - return f'{self.__class__.__name__}({self._data})' - - def __str__(self) -> str: - """Printable representation of Revision data.""" - return str(self._data) - - def __missing__(self, key): + def __missing__(self, key: str, /): """Provide backward compatibility for exceptions.""" - # raise AttributeError instead of KeyError for backward compatibility raise AttributeError( - f"'{type(self).__name__}' object has no attribute '{key}'" + f'{type(self).__name__!r} object has no attribute {key!r}' ) diff --git a/pywikibot/tools/collections.py b/pywikibot/tools/collections.py index 9bec020..96f241d 100644 --- a/pywikibot/tools/collections.py +++ b/pywikibot/tools/collections.py @@ -11,7 +11,7 @@ from collections.abc import Collection, Generator, Iterator, Mapping from contextlib import suppress from itertools import chain -from types import TracebackType +from types import MappingProxyType, TracebackType from typing import Any, NamedTuple
from pywikibot.exceptions import ArgumentDeprecationWarning @@ -20,6 +20,7 @@
__all__ = ( 'CombinedError', + 'DataRecord', 'DequeGenerator', 'EmptyDefault', 'GeneratorWrapper', @@ -380,3 +381,73 @@ def ratio(self) -> float: """Calculate a ratio how many hits can be done within one second.""" return self.hits / self.seconds if self.seconds != 0 else float('inf') + + +class DataRecord(Mapping): + + """A read-only structure holding named data items. + + Each data item can be accessed either by its key or as an attribute + with the attribute name equal to the key. + + For example: + + >>> d = DataRecord(sample='Sample for DataRecord access') + >>> d.sample == d['sample'] + True + >>> d.sample + 'Sample for DataRecord access' + + .. version-added:: 11.6 + """ + + def __init__(self, **kwargs) -> None: + """Initializer.""" + self.normalize(kwargs) + self._data = MappingProxyType(kwargs) + + @staticmethod + def normalize(data: dict[str, Any]) -> None: + """Normalize *data* items in-place. + + This method can be overridden by subclasses to normalize their + specific data items. Changes should be applied directly to the + provided dictionary. + + :param data: The data dictionary to normalize in-place. + """ + + def __len__(self) -> int: + """Return the number of data items.""" + return len(self._data) + + def __getitem__(self, name: str) -> Any: + """Return a data item by name.""" + return self._data[name] + + def __getattr__(self, name: str) -> Any: + """Return a data item by attribute name.""" + if name in self._data: + return self._data[name] + + return self.__missing__(name) + + def __iter__(self): + """Return an iterator over data item names.""" + return iter(self._data) + + def __repr__(self) -> str: + """Return the formal string representation.""" + return f'{type(self).__name__}({self._data})' + + def __str__(self) -> str: + """Return the string representation of the data.""" + return str(self._data) + + def __missing__(self, key: str, /) -> Any: + """Handle missing data items. + + Subclasses can override this method to provide custom handling + for missing data items. + """ + raise KeyError(key)
pywikibot-commits@lists.wikimedia.org