jenkins-bot has submitted this change. ( 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1187941?usp=email )

Change subject: IMPR: Retrieve siteinfo with formatversion 2
......................................................................

IMPR: Retrieve siteinfo with formatversion 2

- retrieve siteinfo with formatversion 2
- add '*' keys to 'namespaces', 'languages', 'namespacealiases'
  and 'skins' for backward compatibility
- 'thumblimits', 'imagelimits' and 'magiclinks' entries of the
  'general' property are normalized to lists for easier use and to
  match the format used in formatversion 1
- update Requests._handle_warnings to catch formatversion 2 warnings
- update Requests.submit to handle formatversion 2 errors
- update siteinfo usage
- update DummySiteinfo and DrySite
- update backports
- import Dict and List from backports
- cast types in Siteinfo._post_process function
- update documentation

Bug: T404301
Change-Id: I29a515d60330c6889a2cb6ceec4df58a417e5c72
---
M pywikibot/backports.py
M pywikibot/data/api/_requests.py
M pywikibot/site/_apisite.py
M pywikibot/site/_datasite.py
M pywikibot/site/_namespace.py
M pywikibot/site/_siteinfo.py
M tests/utils.py
7 files changed, 125 insertions(+), 97 deletions(-)

Approvals:
  Xqt: Looks good to me, approved
  jenkins-bot: Verified




diff --git a/pywikibot/backports.py b/pywikibot/backports.py
index a3b64f7..25b174b 100644
--- a/pywikibot/backports.py
+++ b/pywikibot/backports.py
@@ -53,6 +53,7 @@
         Generator,
         Iterable,
         Iterator,
+        List,
         Mapping,
         Match,
         Pattern,
@@ -70,6 +71,7 @@
     )
     from re import Match, Pattern
     Dict = dict  # type: ignore[misc]
+    List = list  # type: ignore[misc]


 if PYTHON_VERSION < (3, 9, 2):
diff --git a/pywikibot/data/api/_requests.py b/pywikibot/data/api/_requests.py
index a49bc53..035f65c 100644
--- a/pywikibot/data/api/_requests.py
+++ b/pywikibot/data/api/_requests.py
@@ -843,6 +843,10 @@

         .. versionchanged:: 7.2
            Return True to retry the current request and False to resume.
+        .. versionchanged:: 10.5
+           Handle warnings of formatversion 2.
+
+        .. seealso:: :api:`Errors and warnings`

         :meta public:
         """
@@ -853,7 +857,9 @@
         for mod, warning in result['warnings'].items():
             if mod == 'info':
                 continue
-            if '*' in warning:
+            if 'warnings' in warning:  # formatversion 2
+                text = warning['warnings']
+            elif '*' in warning:  # formatversion 1
                 text = warning['*']
             elif 'html' in warning:
                 # bug T51978
@@ -1066,9 +1072,13 @@
                 assert key not in error
                 error[key] = result[key]

-            if '*' in error:
-                # help text returned
-                error['help'] = error.pop('*')
+            # help text returned
+            # see also: https://www.mediawiki.org/wiki/API:Errors_and_warnings
+            if 'docref' in error:
+                error['help'] = error.pop('docref')  # formatversion 2
+            elif '*' in error:
+                error['help'] = error.pop('*')  # formatversion 1
+
             code = error.setdefault('code', 'Unknown')
             info = error.setdefault('info', None)

diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py
index 627d4d2..ec61c45 100644
--- a/pywikibot/site/_apisite.py
+++ b/pywikibot/site/_apisite.py
@@ -841,7 +841,7 @@

         :raises ValueError: missing "$1" placeholder
         """
-        path = self.siteinfo['general']['articlepath']
+        path = self.siteinfo['articlepath']
         if '$1' not in path:
             raise ValueError(
                 f'Invalid article path "{path}": missing "$1" placeholder')
@@ -864,7 +864,7 @@
             'ca': "(?:[a-zàèéíòóúç·ïü]|'(?!'))*",
             'kaa': "(?:[a-zıʼ’“»]|'(?!'))*",
         }
-        linktrail = self.siteinfo['general']['linktrail']
+        linktrail = self.siteinfo['linktrail']
         if linktrail == '/^()(.*)$/sD':  # empty linktrail
             return ''

@@ -1183,10 +1183,10 @@
         for nsdata in self.siteinfo.get('namespaces', cache=False).values():
             ns = nsdata.pop('id')
             if ns == 0:
-                canonical_name = nsdata.pop('*')
+                custom_name = canonical_name = nsdata.pop('name')
                 custom_name = canonical_name
             else:
-                custom_name = nsdata.pop('*')
+                custom_name = nsdata.pop('name')
                 canonical_name = nsdata.pop('canonical')

             default_case = Namespace.default_case(ns)
@@ -1199,16 +1199,16 @@
             namespace = Namespace(ns, canonical_name, custom_name, **nsdata)
             _namespaces[ns] = namespace

-        for item in self.siteinfo.get('namespacealiases'):
+        for item in self.siteinfo['namespacealiases']:
             ns = int(item['id'])
             try:
                 namespace = _namespaces[ns]
             except KeyError:
                 pywikibot.warning('Broken namespace alias "{}" (id: {}) on {}'
-                                  .format(item['*'], ns, self))
+                                  .format(item['alias'], ns, self))
             else:
-                if item['*'] not in namespace:
-                    namespace.aliases.append(item['*'])
+                if item['alias'] not in namespace:
+                    namespace.aliases.append(item['alias'])

         return _namespaces

@@ -3122,7 +3122,7 @@
         >>> site.is_uploaddisabled()
         True
         """
-        return not self.siteinfo.get('general')['uploadsenabled']
+        return not self.siteinfo['uploadsenabled']

     def stash_info(
         self,
diff --git a/pywikibot/site/_datasite.py b/pywikibot/site/_datasite.py
index cf09ee3..9b3492e 100644
--- a/pywikibot/site/_datasite.py
+++ b/pywikibot/site/_datasite.py
@@ -138,35 +138,32 @@
         raise NoWikibaseEntityError(entity)

     @property
-    def sparql_endpoint(self):
+    def sparql_endpoint(self) -> str | None:
         """Return the sparql endpoint url, if any has been set.

         :return: sparql endpoint url
-        :rtype: str|None
         """
-        return self.siteinfo['general'].get('wikibase-sparql')
+        return self.siteinfo.get('wikibase-sparql')

     @property
-    def concept_base_uri(self):
+    def concept_base_uri(self) -> str:
         """Return the base uri for concepts/entities.

         :return: concept base uri
-        :rtype: str
         """
-        return self.siteinfo['general']['wikibase-conceptbaseuri']
+        return self.siteinfo['wikibase-conceptbaseuri']

-    def geo_shape_repository(self):
+    def geo_shape_repository(self) -> DataSite | None:
         """Return Site object for the geo-shapes repository e.g. commons."""
-        url = self.siteinfo['general'].get('wikibase-geoshapestoragebaseurl')
+        url = self.siteinfo.get('wikibase-geoshapestoragebaseurl')
         if url:
             return pywikibot.Site(url=url, user=self.username())

         return None

-    def tabular_data_repository(self):
+    def tabular_data_repository(self) -> DataSite | None:
         """Return Site object for the tabular-data repository e.g. commons."""
-        url = self.siteinfo['general'].get(
-            'wikibase-tabulardatastoragebaseurl')
+        url = self.siteinfo.get('wikibase-tabulardatastoragebaseurl')
         if url:
             return pywikibot.Site(url=url, user=self.username())

diff --git a/pywikibot/site/_namespace.py b/pywikibot/site/_namespace.py
index 00de8fd..f82c03b 100644
--- a/pywikibot/site/_namespace.py
+++ b/pywikibot/site/_namespace.py
@@ -1,6 +1,6 @@
 """Objects representing Namespaces of MediaWiki site."""
 #
-# (C) Pywikibot team, 2008-2024
+# (C) Pywikibot team, 2008-2025
 #
 # Distributed under the terms of the MIT license.
 #
@@ -323,11 +323,7 @@

 class NamespacesDict(Mapping):

-    """An immutable dictionary containing the Namespace instances.
-
-    It adds a deprecation message when called as the 'namespaces'
-    property of APISite was callable.
-    """
+    """An immutable dictionary containing the Namespace instances."""

     def __init__(self, namespaces) -> None:
         """Create new dict using the given namespaces."""
diff --git a/pywikibot/site/_siteinfo.py b/pywikibot/site/_siteinfo.py
index 389947a..46c6b9b 100644
--- a/pywikibot/site/_siteinfo.py
+++ b/pywikibot/site/_siteinfo.py
@@ -11,9 +11,10 @@
 import re
 from collections.abc import Container
 from contextlib import suppress
-from typing import TYPE_CHECKING, Any, Literal
+from typing import TYPE_CHECKING, Any, Literal, cast

 import pywikibot
+from pywikibot.backports import Dict, List
 from pywikibot.exceptions import APIError
 from pywikibot.tools.collections import EMPTY_DEFAULT

@@ -24,43 +25,43 @@

 class Siteinfo(Container):

-    """A 'dictionary' like container for siteinfo.
+    """A dictionary-like container for siteinfo.

     This class queries the server to get the requested siteinfo
-    property. Optionally it can cache this directly in the instance so
-    that later requests don't need to query the server.
+    property. Results can be cached in the instance to avoid repeated
+    queries.

-    All values of the siteinfo property 'general' are directly
-    available.
+    All values of the 'general' property  are directly available.
+
+    .. versionchanged:: 10.5
+       formatversion 2 is used for API calls.
+
+    .. admonition:: Compatibility note
+       :class: note
+
+       For formatversion 2, some siteinfo data structures differ from
+       version 1. Fallback '*' keys are added in the data structure for
+       'namespaces', 'languages', 'namespacealiases' and 'skins'
+       properties for backwards compatibility. These fallbacks may be
+       removed in future versions of Pywikibot.
+
+       The 'thumblimits', 'imagelimits' and 'magiclinks' entries of the
+       'general' property are normalized to lists for easier use and to
+       match the format used in formatversion 1. For example:
+
+       :code:`'thumblimits': [120, 150, 180, 200, 220, 250, 300, 400]`
+
+    .. deprecated:: 10.5
+       Accessing the fallback '*' keys in 'languages', 'namespaces',
+       'namespacealiases', and 'skins' properties are deprecated and
+       will be removed in a future release of Pywikibot.
+
+    .. seealso:: :api:`siteinfo`
     """

     WARNING_REGEX = re.compile(r'Unrecognized values? for parameter '
                                r'["\']siprop["\']: (.+?)\.?')

-    # Until we get formatversion=2, we have to convert empty-string properties
-    # into booleans so they are easier to use.
-    BOOLEAN_PROPS = {
-        'general': [
-            'imagewhitelistenabled',
-            'langconversion',
-            'titleconversion',
-            'rtl',
-            'readonly',
-            'writeapi',
-            'variantarticlepath',
-            'misermode',
-            'uploadsenabled',
-        ],
-        'namespaces': [  # for each namespace
-            'subpages',
-            'content',
-            'nonincludable',
-        ],
-        'magicwords': [  # for each magicword
-            'case-sensitive',
-        ],
-    }
-
     def __init__(self, site: APISite) -> None:
         """Initialize Siteinfo for a given site with an empty cache."""
         self._site = site
@@ -81,35 +82,40 @@

         Modifies *data* in place.

+        .. versionchanged:: 10.5
+           Modify *data* for formatversion 1 compatibility and easier
+           to use lists.
+
         :param prop: The siteinfo property name (e.g., 'general',
             'namespaces', 'magicwords')
         :param data: The raw data returned from the server
+
+        :meta public:
         """
         # Be careful with version tests inside this here as it might need to
         # query this method to actually get the version number

-        # Convert boolean props from empty strings to actual boolean values
-        if prop not in Siteinfo.BOOLEAN_PROPS:
-            return
-
-        bool_props = Siteinfo.BOOLEAN_PROPS[prop]
         if prop == 'general':
-            # Direct properties of 'general'
-            for p in bool_props:
-                data[p] = p in data
-        else:
-            # 'namespaces' (dict) or 'magicwords' (list of dicts)
-            items: list[dict[str, Any]]
-            if isinstance(data, dict):
-                items = list(data.values())
-            elif isinstance(data, list):
-                items = data
-            else:
-                return  # unexpected format
-
-            for item in items:
-                for p in bool_props:
-                    item[p] = p in item
+            data = cast(Dict[str, Any], data)
+            for key in 'thumblimits', 'imagelimits':
+                data[key] = list(data[key].values())
+            data['magiclinks'] = [k for k, v in data['magiclinks'].items()
+                                  if v]
+        elif prop == 'namespaces':
+            data = cast(Dict[str, Any], data)
+            for ns_info in data.values():
+                ns_info['*'] = ns_info['name']
+        elif prop in ('languages', 'namespacealiases'):
+            data = cast(List[Dict[str, Any]], data)
+            for ns_info in data:
+                key = 'name' if 'name' in ns_info else 'alias'
+                ns_info['*'] = ns_info[key]
+        elif prop == 'skins':
+            data = cast(List[Dict[str, Any]], data)
+            for ns_info in data:
+                ns_info['*'] = ns_info['name']
+                for key in 'default', 'unusable':
+                    ns_info.setdefault(key, False)

     def _get_siteinfo(self, prop, expiry) -> dict:
         """Retrieve one or more siteinfo properties from the server.
@@ -144,7 +150,10 @@
             expiry=pywikibot.config.API_config_expiry
             if expiry is False else expiry,
             parameters={
-                'action': 'query', 'meta': 'siteinfo', 'siprop': props,
+                'action': 'query',
+                'meta': 'siteinfo',
+                'siprop': props,
+                'formatversion': 2,
             }
         )

@@ -168,7 +177,7 @@
                 return results
             raise

-        result = {}
+        result: dict[str, tuple[Any, datetime.datetime | Literal[False]]] = {}
         if invalid_properties:
             for invalid_prop in invalid_properties:
                 result[invalid_prop] = (EMPTY_DEFAULT, False)
@@ -334,18 +343,27 @@

         return True

-    def __contains__(self, key: str) -> bool:
-        """Return whether the value is in Siteinfo container.
+    def __contains__(self, key: object) -> bool:
+        """Check whether the given key is present in the Siteinfo container.
+
+        This method implements the Container protocol and allows usage
+        like `key in container`.Only string keys are valid. Non-string
+        keys always return False.

         .. versionchanged:: 7.1
            Previous implementation only checked for cached keys.
-        """
-        try:
-            self[key]
-        except KeyError:
-            return False

-        return True
+        :param key: The key to check for presence. Should be a string.
+        :return: True if the key exists in the container, False otherwise.
+
+        :meta public:
+        """
+        if isinstance(key, str):
+            with suppress(KeyError):
+                self[key]
+                return True
+
+        return False

     def is_recognised(self, key: str) -> bool | None:
         """Return if 'key' is a valid property name.
diff --git a/tests/utils.py b/tests/utils.py
index 44d8227..cf5860f 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -261,16 +261,21 @@
         self._cache[key] = (value, False)

     def get(self, key, get_default=True, cache=True, expiry=False):
-        """Return dry data."""
+        """Return dry cached data or default value."""
         # Default values are always expired, so only expiry=False doesn't force
         # a reload
         force = expiry is not False
-        if not force and key in self._cache:
-            loaded = self._cache[key]
-            if not loaded[1] and not get_default:
+        if not force and (key in self._cache or 'general' in self._cache):
+            try:
+                value, is_default = self._cache[key]
+            except KeyError:
+                value, is_default = self._cache['general']
+                value = value[key]
+
+            if not is_default and not get_default:
                 raise KeyError(key)

-            return loaded[0]
+            return value

         if get_default:
             default = EMPTY_DEFAULT
@@ -343,7 +348,7 @@
         self._siteinfo._cache['case'] = (
             'case-sensitive' if self.family.name == 'wiktionary' else
             'first-letter', True)
-        self._siteinfo._cache['mainpage'] = 'Main Page'
+        self._siteinfo._cache['mainpage'] = ('Main Page', True)
         extensions = []
         if self.family.name == 'wikisource':
             extensions.append({'name': 'ProofreadPage'})

--
To view, visit 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1187941?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: I29a515d60330c6889a2cb6ceec4df58a417e5c72
Gerrit-Change-Number: 1187941
Gerrit-PatchSet: 14
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