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."""