jenkins-bot has submitted this change. (
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1301480?usp=email )
Change subject: IMPR: Add support for temporary accounts
......................................................................
IMPR: Add support for temporary accounts
- Add new methods: is_named(), is_temporary() and temp_expired()
- temp_expired() relies on the 'tempexpired' property from the API 'users'
module. This property is only available when temporary accounts are
enabled on the wiki.
- use the new is_named() method in uploadedImages
- Site.allusers() method was improved; end, withedits, active_only,
named_only, temp_only and reverse option were added; this enables
filtering for named and temporary users.
- tests were added and updated
To support optional user properties, add an `extra_props` parameter to
User.getprops() and Site.users(), allowing additional API user properties
to be requested. temp_expired() only requests the 'tempexpired' property
when the user is a temporary account. This avoids requesting
temporary-account-specific properties for non-temporary users and avoids
API warnings about Unrecognized property values.
Bug: T418983
Change-Id: I5b48c65dbd967b1e8bda66e59f3b9d58a7a28295
---
M pywikibot/page/_user.py
M pywikibot/site/_generators.py
M tests/user_tests.py
3 files changed, 194 insertions(+), 36 deletions(-)
Approvals:
jenkins-bot: Verified
Xqt: Looks good to me, approved
diff --git a/pywikibot/page/_user.py b/pywikibot/page/_user.py
index 2661a9e..b5bdf8c 100644
--- a/pywikibot/page/_user.py
+++ b/pywikibot/page/_user.py
@@ -6,7 +6,7 @@
"""Object representing a Wiki user."""
from __future__ import annotations
-from collections.abc import Generator
+from collections.abc import Generator, Iterable
from typing import Any
import pywikibot
@@ -30,13 +30,27 @@
"""A class that represents a Wiki user.
- This class also represents the Wiki page User:<username>
+ This class also represents the Wiki page ``User:<username>``
+
+ A user object represents a user account, which may be:
+
+ - named (regular) account (see: :meth:`is_named`)
+ - temporary account (see: :meth:`is_temporary`)
+ - anonymous IP user (see: :meth:`isAnonymous`)
+ - CIDR range (see: :meth:`is_CIDR`)
+
+ .. note:: This class inherits from :class:`Page`. Therefore,
+ :meth:`exists()<BasePage.exists>` determines whether the wiki
+ page exists, not whether the user account exists. Use
+ :meth:`is_named`, :meth:`is_temporary` :meth:`isAnonymous`,
+ :meth:`is_CIDR` or :meth:`isRegistered` to determine the account
+ type.
"""
def __init__(self, source, title: str = '') -> None:
"""Initializer for a User object.
- All parameters are the same as for Page() Initializer.
+ All parameters are the same as for ``Page()`` Initializer.
"""
self._isAutoblock = True
if title.startswith('#'):
@@ -68,15 +82,22 @@
def isRegistered(self, force: bool = False) -> bool: # noqa: N802
"""Determine if the user is registered on the site.
- It is possible to have a page named User:xyz and not have
- a corresponding user with username xyz.
+ It is possible to have a page named ``User:xyz`` and not have a
+ corresponding user with username xyz.
- The page does not need to exist for this method to return
- True.
+ This method checks whether the username corresponds to a valid
+ named or temporary account. The user page does not need to exist
+ for this method to return True. Use :meth:`exists()
+ <BasePage.exists>` to check whether the user page exists.
- .. seealso:: :meth:`isAnonymous`
+ .. seealso::
+ - :meth:`isAnonymous`
+ - :meth:`is_temporary`
+ - :meth:`is_named`
:param force: If True, forces reloading the data from API
+ :return: True if the user is either a named (regular) user or a
+ temporary account.
"""
# T135828: the registration timestamp may be None but the key exists
return (not self.isAnonymous()
@@ -87,6 +108,8 @@
.. seealso::
- :meth:`isRegistered`
+ - :meth:`is_temporary`
+ - :meth:`is_named`
- :meth:`is_CIDR`
- :func:`tools.is_ip_address`
"""
@@ -103,20 +126,79 @@
"""
return is_ip_network(self.username)
- def getprops(self, force: bool = False) -> dict[str, Any]:
- """Return a properties about the user.
+ def is_named(self, *, force: bool = False) -> bool:
+ """Determine if the user is a regular named account.
- .. version-changed:: 9.0
- detect range blocks
+ A named account is neither an IP nor a temporary account.
+
+ .. version-added:: 11.4
+ .. seealso::
+ - :meth:`isRegistered`
+ - :meth:`isAnonymous`
+ - :meth:`is_temporary`
:param force: If True, forces reloading the data from API
"""
- if force and hasattr(self, '_userprops'):
+ return self.isRegistered(force) and not self.is_temporary()
+
+ def is_temporary(self) -> bool:
+ """Determine if the user is a temporary account.
+
+ .. version-added:: 11.4
+ .. seealso::
+ - :meth:`isRegistered`
+ - :meth:`isAnonymous`
+ - :meth:`is_named`
+ - :meth:`temp_expired`
+ """
+ return 'temp' in self.groups()
+
+ def temp_expired(self, force: bool = False) -> bool | None:
+ """Indicates whether the temporary account has expired or not.
+
+ If account isn't temporary, None is returned.
+
+ .. version-added:: 11.4
+ .. seealso:: :meth:`is_temporary`
+
+ :param force: If True, forces reloading the data from API
+ """
+ if not self.is_temporary():
+ return None
+
+ return 'tempexpired' in self.getprops(force, ['tempexpired'])
+
+ def getprops(
+ self,
+ force: bool = False,
+ extra_props: Iterable[str] = ()
+ ) -> dict[str, Any]:
+ """Return user properties.
+
+ .. version-changed:: 9.0
+ detect range blocks
+ .. versionchanged:: 11.4
+ Added the *extra_props* parameter.
+
+ :param force: If True, forces reloading the data from API
+ :param extra_props: Additional user properties to request.
+ """
+ if not hasattr(self, '_additional_props'):
+ self._additional_props: set[str] = set()
+
+ new_props = False
+ if extra_props:
+ missing = set(extra_props) - self._additional_props
+ new_props = bool(missing)
+ self._additional_props.update(missing)
+
+ if (force or new_props) and hasattr(self, '_userprops'):
self._userprops: dict[str, Any]
del self._userprops
if not hasattr(self, '_userprops'):
- self._userprops = next(self.site.users([self.username]))
+ self._userprops = next(
+ self.site.users([self.username], self._additional_props))
if self.isAnonymous() or self.is_CIDR():
r = next(self.site.blocks(iprange=self.username, total=1),
None)
@@ -504,8 +586,9 @@
:param total: Limit result to this number of pages
"""
- if not self.isRegistered():
+ if not self.is_named():
return
+
for item in self.logevents(logtype='upload', total=total):
yield (item.page(),
str(item.timestamp()),
diff --git a/pywikibot/site/_generators.py b/pywikibot/site/_generators.py
index 62be750..f9fe056 100644
--- a/pywikibot/site/_generators.py
+++ b/pywikibot/site/_generators.py
@@ -1086,34 +1086,69 @@
yield from self._bots.values()
+ @deprecated_signature(since='11.4.0')
def allusers(
self,
- start: str = '!',
+ *,
+ start: str = '',
prefix: str = '',
group: str | None = None,
total: int | None = None,
- ) -> Iterable[dict[str, str | list[str]]]:
+ end: str = '',
+ withedits: bool = False,
+ active_only: bool = False,
+ named_only: bool = False,
+ temp_only: bool = False,
+ reverse: bool = False,
+ ) -> Iterable[dict[str, str | int | list[str]]]:
"""Iterate registered users, ordered by username.
- Iterated values are dicts containing 'name', 'editcount',
- 'registration', and (sometimes) 'groups' keys. 'groups' will be
- present only if the user is a member of at least 1 group, and
- will be a list of str; all the other values are str and should
- always be present.
+ Iterated values are dicts containing ``'name'``, ``'editcount'``,
+ ``'registration'``, and (sometimes) ``'groups'`` and
+ ``'recentactions'`` keys. ``'groups'`` will be present only if
+ the user is a member of at least 1 group, and will be a list of
+ strings; ``'recentactions'`` will be given if *active_only*
+ parameter is set and will contain an integer representing recent
+ activity. All the other values are strings and should always be
+ present.
+
+ .. version-changed:: 11.4
+ All parameters are keyword-only. The *end*, *withedits*,
+ *active_only*, *named_only*, *temp_only* and *reverse*
+ parameters were added.
.. seealso:: :api:`Allusers`
- :param start: Start at this username (name need not exist)
- :param prefix: Only iterate usernames starting with this substring
- :param group: Only iterate users that are members of this group
+ :param start: Start at this username (name need not exist).
+ :param prefix: Only iterate usernames starting with this substring.
+ :param group: Only iterate users that are members of this group.
+ :param total: Maximum number of pages to retrieve in total.
+ :param end: The username to stop enumerating at (name need not exist).
+ :param withedits: Only list users who have made edits.
+ :param active_only: Only list users active in the last 30 days.
+ :param named_only: Only list users of named accounts.
+ :param temp_only: Only list users of temporary accounts.
+ :param reverse: If True, iterate in reverse lexicographic order
"""
+ if start and end:
+ self.assert_valid_iter_params(
+ 'allusers', start, end, reverse, is_ts=False)
augen = self._generator(api.ListGenerator, type_arg='allusers',
+ aufrom=start or None,
+ auto=end or None,
+ auprefix=prefix or None,
auprop='editcount|groups|registration',
- aufrom=start, total=total)
- if prefix:
- augen.request['auprefix'] = prefix
+ auwitheditsonly=withedits,
+ auactiveusers=active_only,
+ auexcludenamed=temp_only,
+ auexcludetemp=named_only,
+ total=total)
if group:
augen.request['augroup'] = group
+
+ if reverse:
+ augen.request['audir'] = 'descending'
+
return augen
def allimages(
@@ -1865,15 +1900,33 @@
def users(
self,
usernames: Iterable[str],
+ extra_props: Iterable[str] = (),
) -> Iterable[dict[str, Any]]:
"""Iterate info about a list of users by name or IP.
+ By default, the following user properties are requested:
+ ``blockinfo``, ``gender``, ``groups``, ``editcount``,
+ ``registration``, ``rights``, ``emailable``, and ``tempexpired``.
+ Additional properties may be specified with *extra_props* and are
+ appended to the default properties.
+
+ .. versionchanged:: 11.4
+ Added the *extra_props* parameter.
.. seealso:: :api:`Users`
:param usernames: A list of user names
+ :param extra_props: Additional user properties to request.
"""
- usprop = ['blockinfo', 'gender', 'groups', 'editcount', 'registration',
- 'rights', 'emailable']
+ usprop = [
+ 'blockinfo',
+ 'groups',
+ 'rights',
+ 'editcount',
+ 'registration',
+ 'emailable',
+ 'gender',
+ ]
+ usprop.extend(extra_props)
return api.ListGenerator(
'users', site=self,
parameters={'ususers': usernames, 'usprop': usprop}
diff --git a/tests/user_tests.py b/tests/user_tests.py
index 2edc3ab..9b6da3c 100755
--- a/tests/user_tests.py
+++ b/tests/user_tests.py
@@ -33,6 +33,8 @@
self.assertEqual(user.gender(), 'unknown')
self.assertFalse(user.is_thankable)
self.assertIn(prop, user.getprops())
+ self.assertFalse(user.is_temporary())
+ self.assertFalse(user.is_named(), f'{user} is a named user')
def test_anonymous_user(self) -> None:
"""Test registered user."""
@@ -52,19 +54,24 @@
self._tests_unregistered_user(user)
self.assertFalse(user.isAnonymous())
- def test_registered_user(self) -> None:
+ def _test_registered_user(self, user) -> None:
"""Test registered user."""
- user = User(self.site, 'Xqt')
self.assertEqual(user.title(with_ns=False), user.username)
self.assertTrue(user.isRegistered())
self.assertFalse(user.isAnonymous())
+ self.assertEqual(user.gender(), 'unknown')
+ self.assertIn('userid', user.getprops())
+
+ def test_registered_user(self) -> None:
+ """Test registered user."""
+ user = User(self.site, 'Xqt')
+ self._test_registered_user(user)
+ self.assertTrue(user.is_named())
self.assertIsInstance(user.registration(), pywikibot.Timestamp)
self.assertGreater(user.editCount(), 0)
self.assertFalse(user.is_blocked())
self.assertFalse(user.is_locked())
self.assertTrue(user.isEmailable())
- self.assertEqual(user.gender(), 'unknown')
- self.assertIn('userid', user.getprops())
self.assertEqual(user.getprops()['userid'], 287832)
self.assertEqual(user.pageid, 6927779)
self.assertEqual(user.getUserPage(),
@@ -93,8 +100,8 @@
def test_registered_user_without_timestamp(self) -> None:
"""Test registered user when registration timestamp is None."""
user = User(self.site, 'Ulfb')
- self.assertTrue(user.isRegistered())
- self.assertFalse(user.isAnonymous())
+ self._test_registered_user(user)
+ self.assertTrue(user.is_named())
self.assertIsNone(user.registration())
self.assertIsNone(user.getprops()['registration'])
self.assertGreater(user.editCount(), 0)
@@ -102,11 +109,24 @@
self.assertIn('userid', user.getprops())
self.assertTrue(user.is_thankable)
+ def test_temporary_user(self) -> None:
+ """Test registered user."""
+ user = User(self.site, '~2026-99999-1')
+ self._test_registered_user(user)
+ self.assertTrue(user.is_temporary())
+ self.assertFalse(user.is_named())
+ self.assertIn('temp', user.groups())
+ self.assertTrue(user.temp_expired())
+ self.assertFalse(user.isEmailable())
+ self.assertIsInstance(user.registration(), pywikibot.Timestamp)
+
def test_female_user(self) -> None:
"""Test female user."""
user = User(self.site, 'Catrin')
self.assertTrue(user.isRegistered())
self.assertFalse(user.isAnonymous())
+ self.assertTrue(user.is_named())
+ self.assertFalse(user.is_temporary())
self.assertGreater(user.editCount(), 0)
self.assertEqual(user.gender(), 'female')
self.assertIn('userid', user.getprops())
@@ -128,6 +148,8 @@
self.assertEqual(user.title(with_ns=False), user.username[1:])
self.assertFalse(user.isRegistered())
self.assertFalse(user.isAnonymous())
+ self.assertFalse(user.is_named())
+ self.assertFalse(user.is_temporary())
self.assertIsNone(user.registration())
self.assertFalse(user.isEmailable())
self.assertIn('invalid', user.getprops())
--
To view, visit
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1301480?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: I5b48c65dbd967b1e8bda66e59f3b9d58a7a28295
Gerrit-Change-Number: 1301480
Gerrit-PatchSet: 4
Gerrit-Owner: Xqt <[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]