Source code for pywikibot.site._namespace

#
# (C) Pywikibot team, 2008-2026
#
# Distributed under the terms of the MIT license.
#
"""Objects representing Namespaces of MediaWiki site."""
from __future__ import annotations

from abc import ABCMeta
from collections.abc import Iterable, Mapping
from enum import IntEnum
from typing import Union

from pywikibot.tools import ComparableMixin, classproperty


SingleNamespaceType = Union[int, str, 'Namespace']
NamespaceArgType = Union[
    SingleNamespaceType,
    Iterable[SingleNamespaceType],
    None,
]


[docs] class BuiltinNamespace(IntEnum): """Builtin namespace enum.""" 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 @property def canonical(self) -> str: """Canonical form of MediaWiki built-in namespace. .. version-added:: 7.1 """ name = '' if self == 0 else self.name.capitalize().replace('_', ' ') return name.replace('Mediawiki', 'MediaWiki')
[docs] class MetaNamespace(ABCMeta): """Metaclass for Namespace attribute settings. .. version-added:: 9.0 """ def __new__(cls, name, bases, dic): """Set Namespace.FOO to BuiltinNamespace.FOO for each builtin ns.""" dic.update(BuiltinNamespace.__members__) return super().__new__(cls, name, bases, dic)
[docs] class Namespace(Iterable, ComparableMixin, metaclass=MetaNamespace): """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. .. version-changed:: 9.0 metaclass from :class:`MetaNamespace` """ # Hints of BuiltinNamespace types added with initializer MEDIA: int SPECIAL: int MAIN: int TALK: int USER: int USER_TALK: int PROJECT: int PROJECT_TALK: int FILE: int FILE_TALK: int MEDIAWIKI: int MEDIAWIKI_TALK: int TEMPLATE: int TEMPLATE_TALK: int HELP: int HELP_TALK: int CATEGORY: int CATEGORY_TALK: int def __init__(self, id, canonical_name: str | None = None, custom_name: str | None = None, aliases: list[str] | None = None, **kwargs) -> None: """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 BuiltinNamespace(self.id).canonical 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 (BuiltinNamespace.FILE, BuiltinNamespace.FILE_TALK): alias = 'Image' if id == BuiltinNamespace.FILE_TALK: alias += ' talk' self.aliases = [alias] else: self.aliases = [] for key, value in kwargs.items(): setattr(self, key, value) @classproperty def canonical_namespaces(cls) -> dict[int, str]: """Return the canonical forms of MediaWiki built-in namespaces. .. version-changed:: 7.1 implemented as classproperty using BuiltinNamespace IntEnum. """ return {item.value: item.canonical for item in BuiltinNamespace} def _distinct(self): if self.custom_name == self.canonical_name: return [self.canonical_name, *self.aliases] 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 == BuiltinNamespace.MAIN: return True name = Namespace.normalize_name(item) if not name: return False return self._contains_lowercase_name(name.lower()) def __bool__(self) -> bool: """Obtain boolean method for Namespace class. This method is implemented to be independent from __len__ method. .. version-added:: 7.0 :return: Always return True like generic object class. """ return True def __len__(self) -> int: """Obtain length of the iterable.""" return len(self._distinct()) 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 == BuiltinNamespace.MAIN: return ':' if id in (BuiltinNamespace.FILE, BuiltinNamespace.CATEGORY): return ':' + name + ':' return name + ':' def __str__(self) -> str: """Return the canonical string representation.""" return self.canonical_prefix()
[docs] def canonical_prefix(self): """Return the canonical name with required colons.""" return Namespace._colons(self.id, self.canonical_name)
[docs] def custom_prefix(self): """Return the custom name with required colons.""" return Namespace._colons(self.id, self.custom_name)
def __int__(self) -> int: """Return the namespace id.""" return self.id def __index__(self) -> int: """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) -> str: """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 + f'={value!r}' for key, value in extra) else: kwargs = '' return (f'{self.__class__.__name__}(id={self.id}, ' f'custom_name={self.custom_name!r}, ' f'canonical_name={self.canonical_name!r}, ' f'aliases={self.aliases!r}{kwargs})')
[docs] @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 in (BuiltinNamespace.SPECIAL, BuiltinNamespace.USER, BuiltinNamespace.USER_TALK, BuiltinNamespace.MEDIAWIKI, BuiltinNamespace.MEDIAWIKI_TALK, ): return 'first-letter' return default_case
[docs] @classmethod def builtin_namespaces(cls, case: str = 'first-letter'): """Return a dict of the builtin namespaces.""" return {e.value: cls(e.value, case=cls.default_case(e.value, case)) for e in BuiltinNamespace}
[docs] @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
[docs] class NamespacesDict(Mapping): """An immutable dictionary containing the Namespace instances.""" def __init__(self, namespaces) -> None: """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: Namespace | int | str) -> Namespace: """Get the namespace with the given key. :param key: Namespace key """ if isinstance(key, (Namespace, int)): try: return self._namespaces[key] except KeyError: raise KeyError(f'{key} is not a known namespace. Maybe you' ' should clear the api cache.') namespace = self.lookup_name(key) if namespace: return namespace return super().__getitem__(key) def __getattr__(self, attr: 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) -> int: """Get the number of namespaces.""" return len(self._namespaces)
[docs] def lookup_name(self, name: str) -> Namespace | None: """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())
[docs] def lookup_normalized_name(self, name: str) -> Namespace | None: """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)
[docs] 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.get(int(ns)) for ns in identifiers] if NotImplemented in result: raise TypeError( f'identifiers contains inappropriate types: {identifiers!r}' ) # 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