jenkins-bot has submitted this change. ( 
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/643054 )

Change subject: [IMPR] move Namespace and NamespacesDict to site/_namespace.py 
file
......................................................................

[IMPR] move Namespace and NamespacesDict to site/_namespace.py file

- move Namespace and NamespacesDict from site.__init__.py
  into its own _namespace.py file
- remove already deprecated static methods Namespace.lookup_name() and
  Namespace.resolve()
- remove namespace_tests.TestNamespaceDictDeprecated because deprecated
  static methods were removed
- update docs

Change-Id: I59fbbebbe8a5af305c63a139f3561c6292c33c0f
---
M docs/api_ref/pywikibot.site.rst
M pywikibot/CONTENT.rst
M pywikibot/site/__init__.py
A pywikibot/site/_namespace.py
M tests/namespace_tests.py
5 files changed, 459 insertions(+), 586 deletions(-)

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



diff --git a/docs/api_ref/pywikibot.site.rst b/docs/api_ref/pywikibot.site.rst
index 9545088..0982439 100644
--- a/docs/api_ref/pywikibot.site.rst
+++ b/docs/api_ref/pywikibot.site.rst
@@ -16,6 +16,11 @@

 .. automodule:: pywikibot.site._interwikimap

+pywikibot.site.\_namespace module
+---------------------------------
+
+.. automodule:: pywikibot._namespace
+
 pywikibot.site.\_siteinfo module
 --------------------------------

diff --git a/pywikibot/CONTENT.rst b/pywikibot/CONTENT.rst
index fc9fa1e..ae12dc4 100644
--- a/pywikibot/CONTENT.rst
+++ b/pywikibot/CONTENT.rst
@@ -117,6 +117,8 @@
     
+----------------------------+------------------------------------------------------+
     | _interwikimap.py           | Objects representing interwiki map of 
MediaWiki site |
     
+----------------------------+------------------------------------------------------+
+    | _namespace.py              | Objects representing Namespaces of 
MediaWiki site    |
+    
+----------------------------+------------------------------------------------------+
     | _siteinfo.py               | Objects representing site info data 
contents.        |
     
+----------------------------+------------------------------------------------------+
     | _tokenwallet.py            | Objects representing api tokens.            
         |
diff --git a/pywikibot/site/__init__.py b/pywikibot/site/__init__.py
index e0d984d..28e6d8e 100644
--- a/pywikibot/site/__init__.py
+++ b/pywikibot/site/__init__.py
@@ -24,7 +24,7 @@
 import uuid

 from collections import defaultdict, namedtuple
-from collections.abc import Iterable, Mapping
+from collections.abc import Iterable
 from contextlib import suppress
 from itertools import zip_longest
 from pywikibot.login import LoginStatus as _LoginStatus
@@ -68,6 +68,7 @@
 )
 from pywikibot.site._decorators import need_extension, need_right, need_version
 from pywikibot.site._interwikimap import _InterwikiMap
+from pywikibot.site._namespace import Namespace, NamespacesDict
 from pywikibot.site._siteinfo import Siteinfo
 from pywikibot.site._tokenwallet import TokenWallet
 from pywikibot.throttle import Throttle
@@ -88,7 +89,6 @@
     normalize_username,
     PYTHON_VERSION,
     remove_last_args,
-    SelfCallMixin,
     SelfCallString,
 )

@@ -109,493 +109,6 @@
     """Page cannot be reserved for writing due to existing lock."""


-class Namespace(Iterable, ComparableMixin):
-
-    """
-    Namespace site data object.
-
-    This is backwards compatible with the structure of entries
-    in site._namespaces which were a list of::
-
-        [customised namespace,
-         canonical namespace name?,
-         namespace alias*]
-
-    If the canonical_name is not provided for a namespace between -2
-    and 15, the MediaWiki built-in names are used.
-    Image and File are aliases of each other by default.
-
-    If only one of canonical_name and custom_name are available, both
-    properties will have the same value.
-    """
-
-    MEDIA = -2
-    SPECIAL = -1
-    MAIN = 0
-    TALK = 1
-    USER = 2
-    USER_TALK = 3
-    PROJECT = 4
-    PROJECT_TALK = 5
-    FILE = 6
-    FILE_TALK = 7
-    MEDIAWIKI = 8
-    MEDIAWIKI_TALK = 9
-    TEMPLATE = 10
-    TEMPLATE_TALK = 11
-    HELP = 12
-    HELP_TALK = 13
-    CATEGORY = 14
-    CATEGORY_TALK = 15
-
-    # These are the MediaWiki built-in names for MW 1.14+.
-    # Namespace prefixes are always case-insensitive, but the
-    # canonical forms are capitalized.
-    canonical_namespaces = {
-        -2: 'Media',
-        -1: 'Special',
-        0: '',
-        1: 'Talk',
-        2: 'User',
-        3: 'User talk',
-        4: 'Project',
-        5: 'Project talk',
-        6: 'File',
-        7: 'File talk',
-        8: 'MediaWiki',
-        9: 'MediaWiki talk',
-        10: 'Template',
-        11: 'Template talk',
-        12: 'Help',
-        13: 'Help talk',
-        14: 'Category',
-        15: 'Category talk',
-    }
-
-    @deprecated_args(use_image_name=None)
-    def __init__(self, id, canonical_name=None, custom_name=None,
-                 aliases=None, **kwargs):
-        """Initializer.
-
-        @param custom_name: Name defined in server LocalSettings.php
-        @type custom_name: str
-        @param canonical_name: Canonical name
-        @type canonical_name: str
-        @param aliases: Aliases
-        @type aliases: list of str
-        """
-        self.id = id
-        canonical_name = canonical_name or self.canonical_namespaces.get(id)
-
-        assert custom_name is not None or canonical_name is not None, \
-            'Namespace needs to have at least one name'
-
-        self.custom_name = custom_name \
-            if custom_name is not None else canonical_name
-        self.canonical_name = canonical_name \
-            if canonical_name is not None else custom_name
-
-        if aliases:
-            self.aliases = aliases
-        elif id in (6, 7):
-            alias = 'Image'
-            if id == 7:
-                alias += ' talk'
-            self.aliases = [alias]
-        else:
-            self.aliases = []
-
-        for key, value in kwargs.items():
-            setattr(self, key, value)
-
-    def _distinct(self):
-        if self.custom_name == self.canonical_name:
-            return [self.canonical_name] + self.aliases
-        else:
-            return [self.custom_name, self.canonical_name] + self.aliases
-
-    def _contains_lowercase_name(self, name):
-        """Determine a lowercase normalised name is a name of this namespace.
-
-        @rtype: bool
-        """
-        return name in (x.lower() for x in self._distinct())
-
-    def __contains__(self, item: str) -> bool:
-        """Determine if item is a name of this namespace.
-
-        The comparison is case insensitive, and item may have a single
-        colon on one or both sides of the name.
-
-        @param item: name to check
-        """
-        if item == '' and self.id == 0:
-            return True
-
-        name = Namespace.normalize_name(item)
-        if not name:
-            return False
-
-        return self._contains_lowercase_name(name.lower())
-
-    def __len__(self):
-        """Obtain length of the iterable."""
-        if self.custom_name == self.canonical_name:
-            return len(self.aliases) + 1
-        else:
-            return len(self.aliases) + 2
-
-    def __iter__(self):
-        """Return an iterator."""
-        return iter(self._distinct())
-
-    def __getitem__(self, index):
-        """Obtain an item from the iterable."""
-        if self.custom_name != self.canonical_name:
-            if index == 0:
-                return self.custom_name
-            index -= 1
-
-        return self.canonical_name if index == 0 else self.aliases[index - 1]
-
-    @staticmethod
-    def _colons(id, name):
-        """Return the name with required colons, depending on the ID."""
-        if id == 0:
-            return ':'
-
-        if id in (6, 14):
-            return ':' + name + ':'
-
-        return name + ':'
-
-    def __str__(self):
-        """Return the canonical string representation."""
-        return self.canonical_prefix()
-
-    def canonical_prefix(self):
-        """Return the canonical name with required colons."""
-        return Namespace._colons(self.id, self.canonical_name)
-
-    def custom_prefix(self):
-        """Return the custom name with required colons."""
-        return Namespace._colons(self.id, self.custom_name)
-
-    def __int__(self):
-        """Return the namespace id."""
-        return self.id
-
-    def __index__(self):
-        """Return the namespace id."""
-        return self.id
-
-    def __hash__(self):
-        """Return the namespace id."""
-        return self.id
-
-    def __eq__(self, other):
-        """Compare whether two namespace objects are equal."""
-        if isinstance(other, int):
-            return self.id == other
-
-        if isinstance(other, Namespace):
-            return self.id == other.id
-
-        if isinstance(other, str):
-            return other in self
-
-        return False
-
-    def __ne__(self, other):
-        """Compare whether two namespace objects are not equal."""
-        return not self.__eq__(other)
-
-    def __mod__(self, other):
-        """Apply modulo on the namespace id."""
-        return self.id.__mod__(other)
-
-    def __sub__(self, other):
-        """Apply subtraction on the namespace id."""
-        return self.id - other
-
-    def __add__(self, other):
-        """Apply addition on the namespace id."""
-        return self.id + other
-
-    def _cmpkey(self):
-        """Return the ID as a comparison key."""
-        return self.id
-
-    def __repr__(self):
-        """Return a reconstructable representation."""
-        standard_attr = ['id', 'custom_name', 'canonical_name', 'aliases']
-        extra = [(key, self.__dict__[key])
-                 for key in sorted(self.__dict__)
-                 if key not in standard_attr]
-
-        if extra:
-            kwargs = ', ' + ', '.join(
-                key + '=' + repr(value) for key, value in extra)
-        else:
-            kwargs = ''
-
-        return '%s(id=%d, custom_name=%r, canonical_name=%r, aliases=%r%s)' \
-               % (self.__class__.__name__, self.id, self.custom_name,
-                  self.canonical_name, self.aliases, kwargs)
-
-    @staticmethod
-    def default_case(id, default_case=None):
-        """Return the default fixed case value for the namespace ID."""
-        # https://www.mediawiki.org/wiki/Manual:$wgCapitalLinkOverrides#Warning
-        if id > 0 and id % 2 == 1:  # the talk ns has the non-talk ns case
-            id -= 1
-        if id in (-1, 2, 8):
-            return 'first-letter'
-        else:
-            return default_case
-
-    @classmethod
-    @deprecated_args(use_image_name=None)
-    def builtin_namespaces(cls, use_image_name=None, case='first-letter'):
-        """Return a dict of the builtin namespaces."""
-        if use_image_name is not None:
-            issue_deprecation_warning(
-                'positional argument of "use_image_name"', None, 3,
-                DeprecationWarning, since='20181015')
-
-        return {i: cls(i, case=cls.default_case(i, case))
-                for i in range(-2, 16)}
-
-    @staticmethod
-    def normalize_name(name):
-        """
-        Remove an optional colon before and after name.
-
-        TODO: reject illegal characters.
-        """
-        if name == '':
-            return ''
-
-        name = name.replace('_', ' ')
-        parts = name.split(':', 4)
-        count = len(parts)
-        if count > 3 or (count == 3 and parts[2]):
-            return False
-
-        # Discard leading colon
-        if count >= 2 and not parts[0] and parts[1]:
-            return parts[1].strip()
-
-        if parts[0]:
-            return parts[0].strip()
-
-        return False
-
-    @classmethod
-    @deprecated('NamespacesDict.lookup_name', since='20150703',
-                future_warning=True)
-    def lookup_name(cls, name: str, namespaces=None):  # pragma: no cover
-        """
-        Find the Namespace for a name.
-
-        @param name: Name of the namespace.
-        @param namespaces: namespaces to search
-                           default: builtins only
-        @type namespaces: dict of Namespace
-        @rtype: Namespace or None
-        """
-        if not namespaces:
-            namespaces = cls.builtin_namespaces()
-
-        return NamespacesDict._lookup_name(name, namespaces)
-
-    @staticmethod
-    @deprecated('NamespacesDict.resolve', since='20150703',
-                future_warning=True)
-    def resolve(identifiers, namespaces=None):  # pragma: no cover
-        """
-        Resolve namespace identifiers to obtain Namespace objects.
-
-        Identifiers may be any value for which int() produces a valid
-        namespace id, except bool, or any string which Namespace.lookup_name
-        successfully finds. A numerical string is resolved as an integer.
-
-        @param identifiers: namespace identifiers
-        @type identifiers: iterable of str or Namespace key,
-            or a single instance of those types
-        @param namespaces: namespaces to search (default: builtins only)
-        @type namespaces: dict of Namespace
-        @return: list of Namespace objects in the same order as the
-            identifiers
-        @rtype: list
-        @raises KeyError: a namespace identifier was not resolved
-        @raises TypeError: a namespace identifier has an inappropriate
-            type such as NoneType or bool
-        """
-        if not namespaces:
-            namespaces = Namespace.builtin_namespaces()
-
-        return NamespacesDict._resolve(identifiers, namespaces)
-
-
-class NamespacesDict(Mapping, SelfCallMixin):
-
-    """
-    An immutable dictionary containing the Namespace instances.
-
-    It adds a deprecation message when called as the 'namespaces' property of
-    APISite was callable.
-    """
-
-    _own_desc = 'the namespaces property'
-
-    def __init__(self, namespaces):
-        """Create new dict using the given namespaces."""
-        super().__init__()
-        self._namespaces = namespaces
-        self._namespace_names = {}
-        for namespace in self._namespaces.values():
-            for name in namespace:
-                self._namespace_names[name.lower()] = namespace
-
-    def __iter__(self):
-        """Iterate over all namespaces."""
-        return iter(self._namespaces)
-
-    def __getitem__(self, key):
-        """
-        Get the namespace with the given key.
-
-        @param key: namespace key
-        @type key: Namespace, int or str
-        @rtype: Namespace
-        """
-        if isinstance(key, (Namespace, int)):
-            return self._namespaces[key]
-        else:
-            namespace = self.lookup_name(key)
-            if namespace:
-                return namespace
-
-        return super().__getitem__(key)
-
-    def __getattr__(self, attr):
-        """
-        Get the namespace with the given key.
-
-        @param attr: namespace key
-        @type attr: Namespace, int or str
-        @rtype: Namespace
-        """
-        # lookup_name access _namespaces
-        if attr.isupper():
-            if attr == 'MAIN':
-                return self[0]
-
-            namespace = self.lookup_name(attr)
-            if namespace:
-                return namespace
-
-        return self.__getattribute__(attr)
-
-    def __len__(self):
-        """Get the number of namespaces."""
-        return len(self._namespaces)
-
-    def lookup_name(self, name: str):
-        """
-        Find the Namespace for a name also checking aliases.
-
-        @param name: Name of the namespace.
-        @rtype: Namespace or None
-        """
-        name = Namespace.normalize_name(name)
-        if name is False:
-            return None
-        return self.lookup_normalized_name(name.lower())
-
-    def lookup_normalized_name(self, name: str):
-        """
-        Find the Namespace for a name also checking aliases.
-
-        The name has to be normalized and must be lower case.
-
-        @param name: Name of the namespace.
-        @rtype: Namespace or None
-        """
-        return self._namespace_names.get(name)
-
-    # Temporary until Namespace.lookup_name can be removed
-    @staticmethod
-    def _lookup_name(name, namespaces):
-        name = Namespace.normalize_name(name)
-        if name is False:
-            return None
-        name = name.lower()
-
-        for namespace in namespaces.values():
-            if namespace._contains_lowercase_name(name):
-                return namespace
-
-        return None
-
-    def resolve(self, identifiers):
-        """
-        Resolve namespace identifiers to obtain Namespace objects.
-
-        Identifiers may be any value for which int() produces a valid
-        namespace id, except bool, or any string which Namespace.lookup_name
-        successfully finds. A numerical string is resolved as an integer.
-
-        @param identifiers: namespace identifiers
-        @type identifiers: iterable of str or Namespace key,
-            or a single instance of those types
-        @return: list of Namespace objects in the same order as the
-            identifiers
-        @rtype: list
-        @raises KeyError: a namespace identifier was not resolved
-        @raises TypeError: a namespace identifier has an inappropriate
-            type such as NoneType or bool
-        """
-        return self._resolve(identifiers, self._namespaces)
-
-    # Temporary until Namespace.resolve can be removed
-    @staticmethod
-    def _resolve(identifiers, namespaces):
-        if isinstance(identifiers, (str, Namespace)):
-            identifiers = [identifiers]
-        else:
-            # convert non-iterators to single item list
-            try:
-                iter(identifiers)
-            except TypeError:
-                identifiers = [identifiers]
-
-        # lookup namespace names, and assume anything else is a key.
-        # int(None) raises TypeError; however, bool needs special handling.
-        result = [NotImplemented if isinstance(ns, bool) else
-                  NamespacesDict._lookup_name(ns, namespaces)
-                  if isinstance(ns, str)
-                  and not ns.lstrip('-').isdigit() else
-                  namespaces[int(ns)] if int(ns) in namespaces
-                  else None
-                  for ns in identifiers]
-
-        if NotImplemented in result:
-            raise TypeError('identifiers contains inappropriate types: %r'
-                            % identifiers)
-
-        # Namespace.lookup_name returns None if the name is not recognised
-        if None in result:
-            raise KeyError(
-                'Namespace identifier(s) not recognised: {}'
-                .format(','.join(str(identifier)
-                                 for identifier, ns in zip(identifiers, result)
-                                 if ns is None)))
-
-        return result
-
-
 class BaseSite(ComparableMixin):

     """Site methods that are independent of the communication interface."""
diff --git a/pywikibot/site/_namespace.py b/pywikibot/site/_namespace.py
new file mode 100644
index 0000000..51fc7c2
--- /dev/null
+++ b/pywikibot/site/_namespace.py
@@ -0,0 +1,449 @@
+# -*- coding: utf-8 -*-
+"""Objects representing Namespaces of MediaWiki site."""
+#
+# (C) Pywikibot team, 2008-2020
+#
+# Distributed under the terms of the MIT license.
+#
+from collections.abc import Iterable, Mapping
+from typing import Optional, Union
+
+from pywikibot.tools import (
+    ComparableMixin,
+    deprecated_args,
+    issue_deprecation_warning,
+    PYTHON_VERSION,
+    SelfCallMixin,
+)
+
+if PYTHON_VERSION >= (3, 9):
+    List = list
+else:
+    from typing import List
+
+
+class Namespace(Iterable, ComparableMixin):
+
+    """
+    Namespace site data object.
+
+    This is backwards compatible with the structure of entries
+    in site._namespaces which were a list of::
+
+        [customised namespace,
+         canonical namespace name?,
+         namespace alias*]
+
+    If the canonical_name is not provided for a namespace between -2
+    and 15, the MediaWiki built-in names are used.
+    Image and File are aliases of each other by default.
+
+    If only one of canonical_name and custom_name are available, both
+    properties will have the same value.
+    """
+
+    MEDIA = -2
+    SPECIAL = -1
+    MAIN = 0
+    TALK = 1
+    USER = 2
+    USER_TALK = 3
+    PROJECT = 4
+    PROJECT_TALK = 5
+    FILE = 6
+    FILE_TALK = 7
+    MEDIAWIKI = 8
+    MEDIAWIKI_TALK = 9
+    TEMPLATE = 10
+    TEMPLATE_TALK = 11
+    HELP = 12
+    HELP_TALK = 13
+    CATEGORY = 14
+    CATEGORY_TALK = 15
+
+    # These are the MediaWiki built-in names for MW 1.14+.
+    # Namespace prefixes are always case-insensitive, but the
+    # canonical forms are capitalized.
+    canonical_namespaces = {
+        -2: 'Media',
+        -1: 'Special',
+        0: '',
+        1: 'Talk',
+        2: 'User',
+        3: 'User talk',
+        4: 'Project',
+        5: 'Project talk',
+        6: 'File',
+        7: 'File talk',
+        8: 'MediaWiki',
+        9: 'MediaWiki talk',
+        10: 'Template',
+        11: 'Template talk',
+        12: 'Help',
+        13: 'Help talk',
+        14: 'Category',
+        15: 'Category talk',
+    }
+
+    @deprecated_args(use_image_name=True)
+    def __init__(self, id,
+                 canonical_name: Optional[str] = None,
+                 custom_name: Optional[str] = None,
+                 aliases: Optional[List[str]] = None,
+                 **kwargs):
+        """Initializer.
+
+        @param canonical_name: Canonical name
+        @param custom_name: Name defined in server LocalSettings.php
+        @param aliases: Aliases
+        """
+        self.id = id
+        canonical_name = canonical_name or self.canonical_namespaces.get(id)
+
+        assert custom_name is not None or canonical_name is not None, \
+            'Namespace needs to have at least one name'
+
+        self.custom_name = custom_name \
+            if custom_name is not None else canonical_name
+        self.canonical_name = canonical_name \
+            if canonical_name is not None else custom_name
+
+        if aliases:
+            self.aliases = aliases
+        elif id in (6, 7):
+            alias = 'Image'
+            if id == 7:
+                alias += ' talk'
+            self.aliases = [alias]
+        else:
+            self.aliases = []
+
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+
+    def _distinct(self):
+        if self.custom_name == self.canonical_name:
+            return [self.canonical_name] + self.aliases
+        else:
+            return [self.custom_name, self.canonical_name] + self.aliases
+
+    def _contains_lowercase_name(self, name):
+        """Determine a lowercase normalised name is a name of this namespace.
+
+        @rtype: bool
+        """
+        return name in (x.lower() for x in self._distinct())
+
+    def __contains__(self, item: str) -> bool:
+        """Determine if item is a name of this namespace.
+
+        The comparison is case insensitive, and item may have a single
+        colon on one or both sides of the name.
+
+        @param item: name to check
+        """
+        if item == '' and self.id == 0:
+            return True
+
+        name = Namespace.normalize_name(item)
+        if not name:
+            return False
+
+        return self._contains_lowercase_name(name.lower())
+
+    def __len__(self):
+        """Obtain length of the iterable."""
+        if self.custom_name == self.canonical_name:
+            return len(self.aliases) + 1
+        else:
+            return len(self.aliases) + 2
+
+    def __iter__(self):
+        """Return an iterator."""
+        return iter(self._distinct())
+
+    def __getitem__(self, index):
+        """Obtain an item from the iterable."""
+        if self.custom_name != self.canonical_name:
+            if index == 0:
+                return self.custom_name
+            index -= 1
+
+        return self.canonical_name if index == 0 else self.aliases[index - 1]
+
+    @staticmethod
+    def _colons(id, name):
+        """Return the name with required colons, depending on the ID."""
+        if id == 0:
+            return ':'
+
+        if id in (6, 14):
+            return ':' + name + ':'
+
+        return name + ':'
+
+    def __str__(self):
+        """Return the canonical string representation."""
+        return self.canonical_prefix()
+
+    def canonical_prefix(self):
+        """Return the canonical name with required colons."""
+        return Namespace._colons(self.id, self.canonical_name)
+
+    def custom_prefix(self):
+        """Return the custom name with required colons."""
+        return Namespace._colons(self.id, self.custom_name)
+
+    def __int__(self):
+        """Return the namespace id."""
+        return self.id
+
+    def __index__(self):
+        """Return the namespace id."""
+        return self.id
+
+    def __hash__(self):
+        """Return the namespace id."""
+        return self.id
+
+    def __eq__(self, other):
+        """Compare whether two namespace objects are equal."""
+        if isinstance(other, int):
+            return self.id == other
+
+        if isinstance(other, Namespace):
+            return self.id == other.id
+
+        if isinstance(other, str):
+            return other in self
+
+        return False
+
+    def __ne__(self, other):
+        """Compare whether two namespace objects are not equal."""
+        return not self.__eq__(other)
+
+    def __mod__(self, other):
+        """Apply modulo on the namespace id."""
+        return self.id.__mod__(other)
+
+    def __sub__(self, other):
+        """Apply subtraction on the namespace id."""
+        return self.id - other
+
+    def __add__(self, other):
+        """Apply addition on the namespace id."""
+        return self.id + other
+
+    def _cmpkey(self):
+        """Return the ID as a comparison key."""
+        return self.id
+
+    def __repr__(self):
+        """Return a reconstructable representation."""
+        standard_attr = ['id', 'custom_name', 'canonical_name', 'aliases']
+        extra = [(key, self.__dict__[key])
+                 for key in sorted(self.__dict__)
+                 if key not in standard_attr]
+
+        if extra:
+            kwargs = ', ' + ', '.join(
+                key + '=' + repr(value) for key, value in extra)
+        else:
+            kwargs = ''
+
+        return '%s(id=%d, custom_name=%r, canonical_name=%r, aliases=%r%s)' \
+               % (self.__class__.__name__, self.id, self.custom_name,
+                  self.canonical_name, self.aliases, kwargs)
+
+    @staticmethod
+    def default_case(id, default_case=None):
+        """Return the default fixed case value for the namespace ID."""
+        # https://www.mediawiki.org/wiki/Manual:$wgCapitalLinkOverrides#Warning
+        if id > 0 and id % 2 == 1:  # the talk ns has the non-talk ns case
+            id -= 1
+        if id in (-1, 2, 8):
+            return 'first-letter'
+
+        return default_case
+
+    @classmethod
+    @deprecated_args(use_image_name=True)
+    def builtin_namespaces(cls, use_image_name=None, case='first-letter'):
+        """Return a dict of the builtin namespaces."""
+        if use_image_name is not None:
+            issue_deprecation_warning(
+                'positional argument of "use_image_name"', None, 3,
+                FutureWarning, since='20181015')
+
+        return {i: cls(i, case=cls.default_case(i, case))
+                for i in range(-2, 16)}
+
+    @staticmethod
+    def normalize_name(name):
+        """
+        Remove an optional colon before and after name.
+
+        TODO: reject illegal characters.
+        """
+        if name == '':
+            return ''
+
+        name = name.replace('_', ' ')
+        parts = name.split(':', 4)
+        count = len(parts)
+        if count > 3 or (count == 3 and parts[2]):
+            return False
+
+        # Discard leading colon
+        if count >= 2 and not parts[0] and parts[1]:
+            return parts[1].strip()
+
+        if parts[0]:
+            return parts[0].strip()
+
+        return False
+
+
+class NamespacesDict(Mapping, SelfCallMixin):
+
+    """
+    An immutable dictionary containing the Namespace instances.
+
+    It adds a deprecation message when called as the 'namespaces' property of
+    APISite was callable.
+    """
+
+    _own_desc = 'the namespaces property'
+
+    def __init__(self, namespaces):
+        """Create new dict using the given namespaces."""
+        super().__init__()
+        self._namespaces = namespaces
+        self._namespace_names = {}
+        for namespace in self._namespaces.values():
+            for name in namespace:
+                self._namespace_names[name.lower()] = namespace
+
+    def __iter__(self):
+        """Iterate over all namespaces."""
+        return iter(self._namespaces)
+
+    def __getitem__(self, key: Union[Namespace, int, str]) -> Namespace:
+        """
+        Get the namespace with the given key.
+
+        @param key: namespace key
+        """
+        if isinstance(key, (Namespace, int)):
+            return self._namespaces[key]
+
+        namespace = self.lookup_name(key)
+        if namespace:
+            return namespace
+
+        return super().__getitem__(key)
+
+    def __getattr__(self, attr: Union[Namespace, int, str]) -> Namespace:
+        """
+        Get the namespace with the given key.
+
+        @param attr: namespace key
+        """
+        # lookup_name access _namespaces
+        if attr.isupper():
+            if attr == 'MAIN':
+                return self[0]
+
+            namespace = self.lookup_name(attr)
+            if namespace:
+                return namespace
+
+        return self.__getattribute__(attr)
+
+    def __len__(self):
+        """Get the number of namespaces."""
+        return len(self._namespaces)
+
+    def lookup_name(self, name: str) -> Optional[Namespace]:
+        """
+        Find the Namespace for a name also checking aliases.
+
+        @param name: Name of the namespace.
+        """
+        name = Namespace.normalize_name(name)
+        if name is False:
+            return None
+        return self.lookup_normalized_name(name.lower())
+
+    def lookup_normalized_name(self, name: str) -> Optional[Namespace]:
+        """
+        Find the Namespace for a name also checking aliases.
+
+        The name has to be normalized and must be lower case.
+
+        @param name: Name of the namespace.
+        """
+        return self._namespace_names.get(name)
+
+    def resolve(self, identifiers) -> List[Namespace]:
+        """
+        Resolve namespace identifiers to obtain Namespace objects.
+
+        Identifiers may be any value for which int() produces a valid
+        namespace id, except bool, or any string which Namespace.lookup_name
+        successfully finds. A numerical string is resolved as an integer.
+
+        @param identifiers: namespace identifiers
+        @type identifiers: iterable of str or Namespace key,
+            or a single instance of those types
+        @return: list of Namespace objects in the same order as the
+            identifiers
+        @raises KeyError: a namespace identifier was not resolved
+        @raises TypeError: a namespace identifier has an inappropriate
+            type such as NoneType or bool
+        """
+        if isinstance(identifiers, (str, Namespace)):
+            identifiers = [identifiers]
+        else:
+            # convert non-iterators to single item list
+            try:
+                iter(identifiers)
+            except TypeError:
+                identifiers = [identifiers]
+
+        # lookup namespace names, and assume anything else is a key.
+        # int(None) raises TypeError; however, bool needs special handling.
+        namespaces = self._namespaces
+        result = [NotImplemented if isinstance(ns, bool)
+                  else self._lookup_name(ns)
+                  if isinstance(ns, str) and not ns.lstrip('-').isdigit()
+                  else namespaces[int(ns)] if int(ns) in namespaces
+                  else None
+                  for ns in identifiers]
+
+        if NotImplemented in result:
+            raise TypeError('identifiers contains inappropriate types: %r'
+                            % identifiers)
+
+        # Namespace.lookup_name returns None if the name is not recognised
+        if None in result:
+            raise KeyError(
+                'Namespace identifier(s) not recognised: {}'
+                .format(','.join(str(identifier)
+                                 for identifier, ns in zip(identifiers, result)
+                                 if ns is None)))
+
+        return result
+
+    def _lookup_name(self, name):
+        name = Namespace.normalize_name(name)
+        if name is False:
+            return None
+        name = name.lower()
+
+        for namespace in self._namespaces.values():
+            if namespace._contains_lowercase_name(name):
+                return namespace
+
+        return None
diff --git a/tests/namespace_tests.py b/tests/namespace_tests.py
index 8be5fb6..ae968df 100644
--- a/tests/namespace_tests.py
+++ b/tests/namespace_tests.py
@@ -10,8 +10,7 @@

 from pywikibot.site import Namespace, NamespacesDict

-from tests.aspects import (CapturingTestCase, DeprecationTestCase,
-                           TestCase, unittest)
+from tests.aspects import TestCase, unittest

 # Default namespaces which should work in any MW wiki
 _base_builtin_ns = {
@@ -215,101 +214,6 @@
         self.assertEqual(a, b)


-class TestNamespaceDictDeprecated(CapturingTestCase, DeprecationTestCase):
-
-    """Test static/classmethods in Namespace replaced by NamespacesDict."""
-
-    CONTAINSINAPPROPRIATE_RE = (
-        r'identifiers contains inappropriate types: (.*?)'
-    )
-    INTARGNOTSTRINGORNUMBER_RE = (
-        r'int\(\) argument must be a string(, a bytes-like object)? '
-        r"or a number, not '(.*?)'"
-    )
-    NAMESPACEIDNOTRECOGNISED_RE = (
-        r'Namespace identifier\(s\) not recognised: (.*?)'
-    )
-
-    net = False
-
-    def test_resolve_equal(self):
-        """Test Namespace.resolve success."""
-        namespaces = Namespace.builtin_namespaces()
-        main_ns = namespaces[0]
-        file_ns = namespaces[6]
-        special_ns = namespaces[-1]
-
-        self.assertEqual(Namespace.resolve([6]), [file_ns])
-        self.assertEqual(Namespace.resolve(['File']), [file_ns])
-        self.assertEqual(Namespace.resolve(['6']), [file_ns])
-        self.assertEqual(Namespace.resolve([file_ns]), [file_ns])
-
-        self.assertEqual(Namespace.resolve([file_ns, special_ns]),
-                         [file_ns, special_ns])
-        self.assertEqual(Namespace.resolve([file_ns, file_ns]),
-                         [file_ns, file_ns])
-
-        self.assertEqual(Namespace.resolve(6), [file_ns])
-        self.assertEqual(Namespace.resolve('File'), [file_ns])
-        self.assertEqual(Namespace.resolve('6'), [file_ns])
-        self.assertEqual(Namespace.resolve(file_ns), [file_ns])
-
-        self.assertEqual(Namespace.resolve(0), [main_ns])
-        self.assertEqual(Namespace.resolve('0'), [main_ns])
-
-        self.assertEqual(Namespace.resolve(-1), [special_ns])
-        self.assertEqual(Namespace.resolve('-1'), [special_ns])
-
-        self.assertEqual(Namespace.resolve('File:'), [file_ns])
-        self.assertEqual(Namespace.resolve(':File'), [file_ns])
-        self.assertEqual(Namespace.resolve(':File:'), [file_ns])
-
-        self.assertEqual(Namespace.resolve('Image:'), [file_ns])
-        self.assertEqual(Namespace.resolve(':Image'), [file_ns])
-        self.assertEqual(Namespace.resolve(':Image:'), [file_ns])
-
-    def test_resolve_exceptions(self):
-        """Test Namespace.resolve failure."""
-        self.assertRaisesRegex(TypeError, self.CONTAINSINAPPROPRIATE_RE,
-                               Namespace.resolve, [True])
-        self.assertRaisesRegex(TypeError, self.CONTAINSINAPPROPRIATE_RE,
-                               Namespace.resolve, [False])
-        self.assertRaisesRegex(TypeError, self.INTARGNOTSTRINGORNUMBER_RE,
-                               Namespace.resolve, [None])
-        self.assertRaisesRegex(TypeError, self.CONTAINSINAPPROPRIATE_RE,
-                               Namespace.resolve, True)
-        self.assertRaisesRegex(TypeError, self.CONTAINSINAPPROPRIATE_RE,
-                               Namespace.resolve, False)
-        self.assertRaisesRegex(TypeError, self.INTARGNOTSTRINGORNUMBER_RE,
-                               Namespace.resolve, None)
-
-        self.assertRaisesRegex(KeyError, self.NAMESPACEIDNOTRECOGNISED_RE,
-                               Namespace.resolve, -10)
-        self.assertRaisesRegex(KeyError, self.NAMESPACEIDNOTRECOGNISED_RE,
-                               Namespace.resolve, '-10')
-        self.assertRaisesRegex(KeyError, self.NAMESPACEIDNOTRECOGNISED_RE,
-                               Namespace.resolve, 'foo')
-        self.assertRaisesRegex(KeyError, self.NAMESPACEIDNOTRECOGNISED_RE,
-                               Namespace.resolve, ['foo'])
-
-        self.assertRaisesRegex(KeyError, self.NAMESPACEIDNOTRECOGNISED_RE,
-                               Namespace.resolve, [-10, 0])
-        self.assertRaisesRegex(KeyError, self.NAMESPACEIDNOTRECOGNISED_RE,
-                               Namespace.resolve, [0, 'foo'])
-        self.assertRaisesRegex(KeyError, self.NAMESPACEIDNOTRECOGNISED_RE,
-                               Namespace.resolve, [-10, 0, -11])
-
-    def test_lookup_name(self):
-        """Test Namespace.lookup_name."""
-        file_nses = Namespace.builtin_namespaces()
-
-        for name, ns_id in builtin_ns.items():
-            file_ns = Namespace.lookup_name(name, file_nses)
-            self.assertIsInstance(file_ns, Namespace)
-            with self.disable_assert_capture():
-                self.assertEqual(file_ns.id, ns_id)
-
-
 class TestNamespaceCollections(TestCase):

     """Test how Namespace interact when in collections."""

--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/643054
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: I59fbbebbe8a5af305c63a139f3561c6292c33c0f
Gerrit-Change-Number: 643054
Gerrit-PatchSet: 6
Gerrit-Owner: Xqt <[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

Reply via email to