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 <info(a)gno.de>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/643014 )
Change subject: [bugfix] delta must not be an int but a timedelta type
......................................................................
[bugfix] delta must not be an int but a timedelta type
Bug: T268445
Change-Id: I42bd841700f330c494e60267902acfb177d798fb
---
M pywikibot/data/api.py
1 file changed, 16 insertions(+), 13 deletions(-)
Approvals:
Rubin: Looks good to me, but someone else must approve
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/data/api.py b/pywikibot/data/api.py
index cb6abe1..40dee1a 100644
--- a/pywikibot/data/api.py
+++ b/pywikibot/data/api.py
@@ -3034,7 +3034,8 @@
fail_reason = response.get(self.keyword('reason'), '')
if status == self.keyword('success'):
return ''
- elif status in ('NeedToken', 'WrongToken', 'badtoken'):
+
+ if status in ('NeedToken', 'WrongToken', 'badtoken'):
token = response.get('token')
if token and below_mw_1_27:
# fetched token using action=login
@@ -3049,22 +3050,24 @@
# invalidate superior wiki cookies (T224712)
_invalidate_superior_cookies(self.site.family)
continue
- elif (status == 'Throttled' or status == self.keyword('fail')
- and (response['messagecode'] == 'login-throttled'
- or 'wait' in fail_reason)):
- match = re.search(r'(\d+) (seconds|minutes)', fail_reason)
- if match:
- delta = datetime.timedelta(
- **{match.group(2): int(match.group(1))})
- else:
- delta = 0
+
+ if (status == 'Throttled' or status == self.keyword('fail')
+ and (response['messagecode'] == 'login-throttled'
+ or 'wait' in fail_reason)):
wait = response.get('wait')
if wait:
delta = datetime.timedelta(seconds=int(wait))
+ else:
+ match = re.search(r'(\d+) (seconds|minutes)', fail_reason)
+ if match:
+ delta = datetime.timedelta(
+ **{match.group(2): int(match.group(1))})
+ else:
+ delta = datetime.timedelta()
self._waituntil = datetime.datetime.now() + delta
- break
- else:
- break
+
+ break
+
if 'error' in login_result:
raise APIError(**response)
info = fail_reason
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/643014
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: I42bd841700f330c494e60267902acfb177d798fb
Gerrit-Change-Number: 643014
Gerrit-PatchSet: 1
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: Mpaa <mpaa.wiki(a)gmail.com>
Gerrit-Reviewer: Rubin <rubin.happy(a)gmail.com>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/640929 )
Change subject: [bugfix] Do not strip all whitespaces from title
......................................................................
[bugfix] Do not strip all whitespaces from title
Bug: T197642
Change-Id: Ia482218cc569e566dbd8bfd0927a469667e0a26c
---
M pywikibot/page/__init__.py
1 file changed, 3 insertions(+), 3 deletions(-)
Approvals:
JJMC89: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/page/__init__.py b/pywikibot/page/__init__.py
index fdc1973..4142102 100644
--- a/pywikibot/page/__init__.py
+++ b/pywikibot/page/__init__.py
@@ -5988,9 +5988,9 @@
# "empty" local links can only be self-links
# with a fragment identifier.
- if not t.strip() and not self._is_interwiki:
- raise pywikibot.InvalidTitle('The link does not contain a page '
- 'title')
+ if not t.strip(' ') and not self._is_interwiki: # T197642
+ raise pywikibot.InvalidTitle(
+ 'The link does not contain a page title')
if self._site.namespaces[self._namespace].case == 'first-letter':
t = first_upper(t)
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/640929
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: Ia482218cc569e566dbd8bfd0927a469667e0a26c
Gerrit-Change-Number: 640929
Gerrit-PatchSet: 3
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: Dvorapa <dvorapa(a)seznam.cz>
Gerrit-Reviewer: JJMC89 <JJMC89.Wikimedia(a)gmail.com>
Gerrit-Reviewer: Zhuyifei1999 <zhuyifei1999(a)gmail.com>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged