from __future__ import annotations
import re
from functools import total_ordering
from typing import TYPE_CHECKING, ClassVar, TypeVar
from ._namespace_data import NamespaceData
from .namespace import Namespace
if TYPE_CHECKING:
from .parser import Parser
Self = TypeVar("Self", bound="Title")
[docs]@total_ordering
class Title:
"""
Represents a MediaWiki title.
This class is not meant to be used directly.
Use :meth:`Parser.parse <.parser.Parser.parse>` instead.
"""
__slots__ = ("_name", "_namespace", "_parser")
_extension: ClassVar[re.Pattern[str]] = re.compile(r"(?<=\.)[^.\s]+$")
_parser: Parser
_name: str
_namespace: int
[docs] def __init__(self, name: str, *, namespace: int, parser: Parser) -> None:
"""
Construct a Title object.
:param name: The page name part of the title.
:param namespace: The namespace of the title.
:param parser: The parser which constructed the title.
"""
self._name = name
self._namespace = namespace
self._parser = parser
def __str__(self) -> str:
return self.full_name
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.full_name!r})"
def __hash__(self) -> int:
return hash(self.full_name)
def __lt__(self, other: object) -> bool:
if isinstance(other, Title):
return self.full_name < other.full_name
if isinstance(other, str):
return self.full_name < other
return NotImplemented
def __eq__(self, other: object) -> bool:
if isinstance(other, Title):
return self.full_name == other.full_name
if isinstance(other, str):
return self.full_name == other
return NotImplemented
[docs] def __add__(self, other: str) -> Title:
"""
Add a string to this title's full name
and pass that to the parser.
A :class:`Title` cannot be added to one another
since there is no way to determine the namespace
of the new title.
"""
if not isinstance(other, str):
return NotImplemented
return self._parser.parse(f"{self}{other}")
[docs] def __truediv__(self, other: str) -> Title:
"""
Add ``/`` and ``other`` to the title
and pass that to the parser.
"""
return self + f"/{other}"
@property
def full_name(self) -> str:
"""
The full title (i.e. ``Namespace:Pagename`` or ``Pagename``).
"""
if self.namespace != 0:
return f"{self.namespace_name}:{self._name}"
return self._name
@property
def namespace(self) -> int:
"""
The title's namespace ID.
"""
return self._namespace
@property
def name(self) -> str:
"""
The title without the namespace.
"""
return self._name
@property
def namespace_name(self) -> str:
"""
The localized name of the title's namespace.
"""
return self.namespace_data.name
@property
def namespace_data(self) -> NamespaceData:
"""
An object containing all known information
about the title's namespace.
This is retrieved from the parser.
"""
return self._parser.namespace_data[str(self.namespace)]
@property
def canonical_namespace_name(self) -> str | None:
"""
The canonical name of the title's namespace.
"""
return self.namespace_data.canonical
@property
def associated_namespace(self) -> int | None:
"""
The ID of the talk or subject namespace to which
the title's namespace is associated with.
"""
if self.namespace < 0:
return None
if self.namespace % 2 == 1:
return self.namespace - 1
return self.namespace + 1
@property
def associated_namespace_name(self) -> str | None:
"""
The localized name of the title's associated namespace.
"""
namespace_data = self.associated_namespace_data
if namespace_data is None:
return None
return namespace_data.name
@property
def associated_namespace_data(self) -> NamespaceData | None:
"""
An object containing all known information about
the title's associated namespace or ``None`` if there
is no such namespace.
This is retrieved from the parser.
"""
namespace_id = self.associated_namespace
if namespace_id is None:
return None
return self._parser.namespace_data.get(str(namespace_id))
@property
def in_content_namespace(self) -> bool:
"""
Whether the namespace of the title is a content namespace.
"""
return self.namespace_data.content
@property
def fragments(self) -> tuple[str, ...]:
"""
If the namespace has ``.subpages == True``,
return a list of strings generated from
splitting the title by ``/``. Else,
return the name wrapped in a list.
"""
if self.namespace_data.subpages:
return tuple(self.name.split("/"))
return tuple([self.name])
@property
def root(self: Self) -> Self:
"""
A Title object representing the root title
of this title.
"""
return self.__class__(
self.fragments[0], namespace=self.namespace, parser=self._parser
)
@property
def base(self: Self) -> Self:
"""
A Title object representing the parent title
of this title.
"""
fragments = self.fragments
if len(fragments) == 1:
new_page_name = fragments[0]
else:
new_page_name = "/".join(fragments[:-1])
return self.__class__(
new_page_name, namespace=self.namespace, parser=self._parser
)
@property
def tail(self) -> str:
"""
The rightmost fragment of the title.
"""
# Naming reason: https://superuser.com/q/524724
return self.fragments[-1]
@property
def is_subpage(self) -> bool:
"""
Whether the title has a parent title.
"""
return len(self.fragments) > 1
@property
def extension(self) -> str | None:
"""
The extension part of a file name, if any.
"""
if self.namespace not in (Namespace.FILE, Namespace.FILE_TALK):
return None
match = self._extension.search(self.name)
if match is None:
return None
return match.group(0)
@property
def associated(self: Self) -> Self | None:
"""
The title associated to this title, or ``None``
if there is no such title.
"""
associated_namespace = self.associated_namespace
if associated_namespace is None:
return None
return self.__class__(
self.name, namespace=associated_namespace, parser=self._parser
)
@property
def subject(self: Self) -> Self:
"""
The subject title correspond to this title.
Can be itself if it is a subject title.
"""
associated = self.associated
if associated is None:
return self
if associated.namespace % 2 == 1:
return self
return associated
@property
def talk(self: Self) -> Self | None:
"""
The talk title correspond to this title,
or ``None`` if there is no such title.
"""
if self.namespace < 0:
return None
if self.namespace % 2 == 1:
return self
return self.associated