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]

Reply via email to