| @@ -20,6 +20,7 @@ | |||||
| import re | import re | ||||
| from itertools import chain | from itertools import chain | ||||
| from typing import Any, Callable, Generator, Iterable, Optional, Union, cast, overload | |||||
| from .nodes import ( | from .nodes import ( | ||||
| Argument, | Argument, | ||||
| @@ -55,15 +56,20 @@ class Wikicode(StringMixIn): | |||||
| RECURSE_OTHERS = 2 | RECURSE_OTHERS = 2 | ||||
| def __init__(self, nodes): | |||||
| def __init__(self, nodes: list[Node]): | |||||
| super().__init__() | super().__init__() | ||||
| self._nodes = nodes | self._nodes = nodes | ||||
| def __str__(self): | |||||
| def __str__(self) -> str: | |||||
| return "".join([str(node) for node in self.nodes]) | return "".join([str(node) for node in self.nodes]) | ||||
| @staticmethod | @staticmethod | ||||
| def _get_children(node, contexts=False, restrict=None, parent=None): | |||||
| def _get_children( | |||||
| node: Node, | |||||
| contexts: bool = False, | |||||
| restrict: Optional[type] = None, | |||||
| parent: Optional["Wikicode"] = None, | |||||
| ) -> Generator[Union[tuple[Optional["Wikicode"], Node], Node]]: | |||||
| """Iterate over all child :class:`.Node`\\ s of a given *node*.""" | """Iterate over all child :class:`.Node`\\ s of a given *node*.""" | ||||
| yield (parent, node) if contexts else node | yield (parent, node) if contexts else node | ||||
| if restrict and isinstance(node, restrict): | if restrict and isinstance(node, restrict): | ||||
| @@ -74,14 +80,19 @@ class Wikicode(StringMixIn): | |||||
| yield from sub | yield from sub | ||||
| @staticmethod | @staticmethod | ||||
| def _slice_replace(code, index, old, new): | |||||
| def _slice_replace(code: "Wikicode", index: slice, old: str, new: str) -> None: | |||||
| """Replace the string *old* with *new* across *index* in *code*.""" | """Replace the string *old* with *new* across *index* in *code*.""" | ||||
| nodes = [str(node) for node in code.get(index)] | nodes = [str(node) for node in code.get(index)] | ||||
| substring = "".join(nodes).replace(old, new) | substring = "".join(nodes).replace(old, new) | ||||
| code.nodes[index] = parse_anything(substring).nodes | code.nodes[index] = parse_anything(substring).nodes | ||||
| @staticmethod | @staticmethod | ||||
| def _build_matcher(matches, flags): | |||||
| def _build_matcher( | |||||
| matches: Union[ | |||||
| Callable[[Node], Union[bool, re.Match[str], None]], re.Pattern, str, None | |||||
| ], | |||||
| flags: int, | |||||
| ) -> Callable[[Node], Union[bool, re.Match[str], None]]: | |||||
| """Helper for :meth:`_indexed_ifilter` and others. | """Helper for :meth:`_indexed_ifilter` and others. | ||||
| If *matches* is a function, return it. If it's a regex, return a | If *matches* is a function, return it. If it's a regex, return a | ||||
| @@ -95,8 +106,12 @@ class Wikicode(StringMixIn): | |||||
| return lambda obj: True | return lambda obj: True | ||||
| def _indexed_ifilter( | def _indexed_ifilter( | ||||
| self, recursive=True, matches=None, flags=FLAGS, forcetype=None | |||||
| ): | |||||
| self, | |||||
| recursive: bool = True, | |||||
| matches: Union[Callable[[Node], bool], re.Pattern, str, None] = None, | |||||
| flags: int = FLAGS, | |||||
| forcetype: Optional[type] = None, | |||||
| ) -> Generator[tuple[int, Node]]: | |||||
| """Iterate over nodes and their corresponding indices in the node list. | """Iterate over nodes and their corresponding indices in the node list. | ||||
| The arguments are interpreted as for :meth:`ifilter`. For each tuple | The arguments are interpreted as for :meth:`ifilter`. For each tuple | ||||
| @@ -105,12 +120,13 @@ class Wikicode(StringMixIn): | |||||
| node itself, but will still contain it. | node itself, but will still contain it. | ||||
| """ | """ | ||||
| match = self._build_matcher(matches, flags) | match = self._build_matcher(matches, flags) | ||||
| inodes: Iterable[tuple[int, Node]] | |||||
| if recursive: | if recursive: | ||||
| restrict = forcetype if recursive == self.RECURSE_OTHERS else None | restrict = forcetype if recursive == self.RECURSE_OTHERS else None | ||||
| def getter(i, node): | |||||
| def getter(i: int, node: Node) -> Generator[tuple[int, Node]]: | |||||
| for ch in self._get_children(node, restrict=restrict): | for ch in self._get_children(node, restrict=restrict): | ||||
| yield (i, ch) | |||||
| yield (i, cast(Node, ch)) | |||||
| inodes = chain(*(getter(i, n) for i, n in enumerate(self.nodes))) | inodes = chain(*(getter(i, n) for i, n in enumerate(self.nodes))) | ||||
| else: | else: | ||||
| @@ -119,7 +135,7 @@ class Wikicode(StringMixIn): | |||||
| if (not forcetype or isinstance(node, forcetype)) and match(node): | if (not forcetype or isinstance(node, forcetype)) and match(node): | ||||
| yield (i, node) | yield (i, node) | ||||
| def _is_child_wikicode(self, obj, recursive=True): | |||||
| def _is_child_wikicode(self, obj: "Wikicode", recursive: bool = True) -> bool: | |||||
| """Return whether the given :class:`.Wikicode` is a descendant.""" | """Return whether the given :class:`.Wikicode` is a descendant.""" | ||||
| def deref(nodes): | def deref(nodes): | ||||
| @@ -140,7 +156,9 @@ class Wikicode(StringMixIn): | |||||
| todo += list(node.__children__()) | todo += list(node.__children__()) | ||||
| return False | return False | ||||
| def _do_strong_search(self, obj, recursive=True): | |||||
| def _do_strong_search( | |||||
| self, obj: Union[Node, "Wikicode"], recursive: bool = True | |||||
| ) -> tuple["Wikicode", slice]: | |||||
| """Search for the specific element *obj* within the node list. | """Search for the specific element *obj* within the node list. | ||||
| *obj* can be either a :class:`.Node` or a :class:`.Wikicode` object. If | *obj* can be either a :class:`.Node` or a :class:`.Wikicode` object. If | ||||
| @@ -171,7 +189,9 @@ class Wikicode(StringMixIn): | |||||
| raise TypeError(obj) | raise TypeError(obj) | ||||
| def _do_weak_search(self, obj, recursive): | |||||
| def _do_weak_search( | |||||
| self, obj: Any, recursive: bool | |||||
| ) -> list[tuple[bool, "Wikicode", slice]]: | |||||
| """Search for an element that looks like *obj* within the node list. | """Search for an element that looks like *obj* within the node list. | ||||
| This follows the same rules as :meth:`_do_strong_search` with some | This follows the same rules as :meth:`_do_strong_search` with some | ||||
| @@ -192,7 +212,7 @@ class Wikicode(StringMixIn): | |||||
| if not obj or obj not in self: | if not obj or obj not in self: | ||||
| raise ValueError(obj) | raise ValueError(obj) | ||||
| results = [] | results = [] | ||||
| contexts = [self] | |||||
| contexts: list["Wikicode"] = [self] | |||||
| while contexts: | while contexts: | ||||
| context = contexts.pop() | context = contexts.pop() | ||||
| i = len(context.nodes) - 1 | i = len(context.nodes) - 1 | ||||
| @@ -215,7 +235,9 @@ class Wikicode(StringMixIn): | |||||
| results.append((False, self, slice(0, len(self.nodes)))) | results.append((False, self, slice(0, len(self.nodes)))) | ||||
| return results | return results | ||||
| def _get_tree(self, code, lines, marker, indent): | |||||
| def _get_tree( | |||||
| self, code: "Wikicode", lines: list[str], marker: Any, indent: int | |||||
| ) -> list[str]: | |||||
| """Build a tree to illustrate the way the Wikicode object was parsed. | """Build a tree to illustrate the way the Wikicode object was parsed. | ||||
| The method that builds the actual tree is ``__showtree__`` of ``Node`` | The method that builds the actual tree is ``__showtree__`` of ``Node`` | ||||
| @@ -227,7 +249,7 @@ class Wikicode(StringMixIn): | |||||
| the starting indentation. | the starting indentation. | ||||
| """ | """ | ||||
| def write(*args): | |||||
| def write(*args: str) -> None: | |||||
| """Write a new line following the proper indentation rules.""" | """Write a new line following the proper indentation rules.""" | ||||
| if lines and lines[-1] is marker: # Continue from the last line | if lines and lines[-1] is marker: # Continue from the last line | ||||
| lines.pop() # Remove the marker | lines.pop() # Remove the marker | ||||
| @@ -275,7 +297,7 @@ class Wikicode(StringMixIn): | |||||
| setattr(cls, "filter_" + name, filt) | setattr(cls, "filter_" + name, filt) | ||||
| @property | @property | ||||
| def nodes(self): | |||||
| def nodes(self) -> list[Node]: | |||||
| """A list of :class:`.Node` objects. | """A list of :class:`.Node` objects. | ||||
| This is the internal data actually stored within a :class:`.Wikicode` | This is the internal data actually stored within a :class:`.Wikicode` | ||||
| @@ -284,16 +306,22 @@ class Wikicode(StringMixIn): | |||||
| return self._nodes | return self._nodes | ||||
| @nodes.setter | @nodes.setter | ||||
| def nodes(self, value): | |||||
| def nodes(self, value: Union[list[Node], Any]) -> None: | |||||
| if not isinstance(value, list): | if not isinstance(value, list): | ||||
| value = parse_anything(value).nodes | value = parse_anything(value).nodes | ||||
| self._nodes = value | self._nodes = value | ||||
| @overload | |||||
| def get(self, index: int) -> Node: ... | |||||
| @overload | |||||
| def get(self, index: slice) -> list[Node]: ... | |||||
| def get(self, index): | def get(self, index): | ||||
| """Return the *index*\\ th node within the list of nodes.""" | """Return the *index*\\ th node within the list of nodes.""" | ||||
| return self.nodes[index] | return self.nodes[index] | ||||
| def set(self, index, value): | |||||
| def set(self, index: int, value: Any) -> None: | |||||
| """Set the ``Node`` at *index* to *value*. | """Set the ``Node`` at *index* to *value*. | ||||
| Raises :exc:`IndexError` if *index* is out of range, or | Raises :exc:`IndexError` if *index* is out of range, or | ||||
| @@ -311,7 +339,7 @@ class Wikicode(StringMixIn): | |||||
| else: | else: | ||||
| self.nodes.pop(index) | self.nodes.pop(index) | ||||
| def contains(self, obj): | |||||
| def contains(self, obj: Union[Node, "Wikicode", str]) -> bool: | |||||
| """Return whether this Wikicode object contains *obj*. | """Return whether this Wikicode object contains *obj*. | ||||
| If *obj* is a :class:`.Node` or :class:`.Wikicode` object, then we | If *obj* is a :class:`.Node` or :class:`.Wikicode` object, then we | ||||
| @@ -326,7 +354,7 @@ class Wikicode(StringMixIn): | |||||
| return False | return False | ||||
| return True | return True | ||||
| def index(self, obj, recursive=False): | |||||
| def index(self, obj: Union[Node, "Wikicode", str], recursive: bool = False) -> int: | |||||
| """Return the index of *obj* in the list of nodes. | """Return the index of *obj* in the list of nodes. | ||||
| Raises :exc:`ValueError` if *obj* is not found. If *recursive* is | Raises :exc:`ValueError` if *obj* is not found. If *recursive* is | ||||
| @@ -345,7 +373,7 @@ class Wikicode(StringMixIn): | |||||
| return i | return i | ||||
| raise ValueError(obj) | raise ValueError(obj) | ||||
| def get_ancestors(self, obj): | |||||
| def get_ancestors(self, obj: Union[Node, "Wikicode"]) -> list[Node]: | |||||
| """Return a list of all ancestor nodes of the :class:`.Node` *obj*. | """Return a list of all ancestor nodes of the :class:`.Node` *obj*. | ||||
| The list is ordered from the most shallow ancestor (greatest great- | The list is ordered from the most shallow ancestor (greatest great- | ||||
| @@ -362,7 +390,7 @@ class Wikicode(StringMixIn): | |||||
| object. Will raise :exc:`ValueError` if it wasn't found. | object. Will raise :exc:`ValueError` if it wasn't found. | ||||
| """ | """ | ||||
| def _get_ancestors(code, needle): | |||||
| def _get_ancestors(code: "Wikicode", needle: Node) -> Union[list[Node], None]: | |||||
| for node in code.nodes: | for node in code.nodes: | ||||
| if node is needle: | if node is needle: | ||||
| return [] | return [] | ||||
| @@ -382,7 +410,7 @@ class Wikicode(StringMixIn): | |||||
| raise ValueError(obj) | raise ValueError(obj) | ||||
| return ancestors | return ancestors | ||||
| def get_parent(self, obj): | |||||
| def get_parent(self, obj: Union[Node, "Wikicode"]) -> Union[Node, None]: | |||||
| """Return the direct parent node of the :class:`.Node` *obj*. | """Return the direct parent node of the :class:`.Node` *obj*. | ||||
| This function is equivalent to calling :meth:`.get_ancestors` and | This function is equivalent to calling :meth:`.get_ancestors` and | ||||
| @@ -393,7 +421,7 @@ class Wikicode(StringMixIn): | |||||
| ancestors = self.get_ancestors(obj) | ancestors = self.get_ancestors(obj) | ||||
| return ancestors[-1] if ancestors else None | return ancestors[-1] if ancestors else None | ||||
| def insert(self, index, value): | |||||
| def insert(self, index: int, value: Any) -> None: | |||||
| """Insert *value* at *index* in the list of nodes. | """Insert *value* at *index* in the list of nodes. | ||||
| *value* can be anything parsable by :func:`.parse_anything`, which | *value* can be anything parsable by :func:`.parse_anything`, which | ||||
| @@ -403,7 +431,9 @@ class Wikicode(StringMixIn): | |||||
| for node in reversed(nodes): | for node in reversed(nodes): | ||||
| self.nodes.insert(index, node) | self.nodes.insert(index, node) | ||||
| def insert_before(self, obj, value, recursive=True): | |||||
| def insert_before( | |||||
| self, obj: Union[Node, "Wikicode", str], value: Any, recursive: bool = True | |||||
| ) -> None: | |||||
| """Insert *value* immediately before *obj*. | """Insert *value* immediately before *obj*. | ||||
| *obj* can be either a string, a :class:`.Node`, or another | *obj* can be either a string, a :class:`.Node`, or another | ||||
| @@ -426,7 +456,9 @@ class Wikicode(StringMixIn): | |||||
| obj = str(obj) | obj = str(obj) | ||||
| self._slice_replace(context, index, obj, str(value) + obj) | self._slice_replace(context, index, obj, str(value) + obj) | ||||
| def insert_after(self, obj, value, recursive=True): | |||||
| def insert_after( | |||||
| self, obj: Union[Node, "Wikicode", str], value: Any, recursive: bool = True | |||||
| ) -> None: | |||||
| """Insert *value* immediately after *obj*. | """Insert *value* immediately after *obj*. | ||||
| *obj* can be either a string, a :class:`.Node`, or another | *obj* can be either a string, a :class:`.Node`, or another | ||||
| @@ -449,7 +481,9 @@ class Wikicode(StringMixIn): | |||||
| obj = str(obj) | obj = str(obj) | ||||
| self._slice_replace(context, index, obj, obj + str(value)) | self._slice_replace(context, index, obj, obj + str(value)) | ||||
| def replace(self, obj, value, recursive=True): | |||||
| def replace( | |||||
| self, obj: Union[Node, "Wikicode", str], value: Any, recursive: bool = True | |||||
| ) -> None: | |||||
| """Replace *obj* with *value*. | """Replace *obj* with *value*. | ||||
| *obj* can be either a string, a :class:`.Node`, or another | *obj* can be either a string, a :class:`.Node`, or another | ||||
| @@ -475,7 +509,7 @@ class Wikicode(StringMixIn): | |||||
| else: | else: | ||||
| self._slice_replace(context, index, str(obj), str(value)) | self._slice_replace(context, index, str(obj), str(value)) | ||||
| def append(self, value): | |||||
| def append(self, value: Any) -> None: | |||||
| """Insert *value* at the end of the list of nodes. | """Insert *value* at the end of the list of nodes. | ||||
| *value* can be anything parsable by :func:`.parse_anything`. | *value* can be anything parsable by :func:`.parse_anything`. | ||||
| @@ -484,7 +518,7 @@ class Wikicode(StringMixIn): | |||||
| for node in nodes: | for node in nodes: | ||||
| self.nodes.append(node) | self.nodes.append(node) | ||||
| def remove(self, obj, recursive=True): | |||||
| def remove(self, obj: Union[Node, "Wikicode", str], recursive: bool = True) -> None: | |||||
| """Remove *obj* from the list of nodes. | """Remove *obj* from the list of nodes. | ||||
| *obj* can be either a string, a :class:`.Node`, or another | *obj* can be either a string, a :class:`.Node`, or another | ||||
| @@ -508,7 +542,12 @@ class Wikicode(StringMixIn): | |||||
| else: | else: | ||||
| self._slice_replace(context, index, str(obj), "") | self._slice_replace(context, index, str(obj), "") | ||||
| def matches(self, other): | |||||
| def matches( | |||||
| self, | |||||
| other: Union[ | |||||
| Node, "Wikicode", str, bytes, Iterable[Union[Node, "Wikicode", str, bytes]] | |||||
| ], | |||||
| ) -> bool: | |||||
| """Do a loose equivalency test suitable for comparing page names. | """Do a loose equivalency test suitable for comparing page names. | ||||
| *other* can be any string-like object, including :class:`.Wikicode`, or | *other* can be any string-like object, including :class:`.Wikicode`, or | ||||
| @@ -517,7 +556,9 @@ class Wikicode(StringMixIn): | |||||
| letter's case is normalized. Typical usage is | letter's case is normalized. Typical usage is | ||||
| ``if template.name.matches("stub"): ...``. | ``if template.name.matches("stub"): ...``. | ||||
| """ | """ | ||||
| normalize = lambda s: (s[0].upper() + s[1:]).replace("_", " ") if s else s | |||||
| def normalize(s: str) -> str: | |||||
| return (s[0].upper() + s[1:]).replace("_", " ") if s else s | |||||
| this = normalize(self.strip_code().strip()) | this = normalize(self.strip_code().strip()) | ||||
| if isinstance(other, (str, bytes, Wikicode, Node)): | if isinstance(other, (str, bytes, Wikicode, Node)): | ||||
| @@ -530,7 +571,13 @@ class Wikicode(StringMixIn): | |||||
| return True | return True | ||||
| return False | return False | ||||
| def ifilter(self, recursive=True, matches=None, flags=FLAGS, forcetype=None): | |||||
| def ifilter( | |||||
| self, | |||||
| recursive: bool = True, | |||||
| matches: Union[Callable[[Node], bool], re.Pattern, str, None] = None, | |||||
| flags: int = FLAGS, | |||||
| forcetype: Optional[type] = None, | |||||
| ) -> Generator[Node]: | |||||
| """Iterate over nodes in our list matching certain conditions. | """Iterate over nodes in our list matching certain conditions. | ||||
| If *forcetype* is given, only nodes that are instances of this type (or | If *forcetype* is given, only nodes that are instances of this type (or | ||||
| @@ -557,7 +604,7 @@ class Wikicode(StringMixIn): | |||||
| gen = self._indexed_ifilter(recursive, matches, flags, forcetype) | gen = self._indexed_ifilter(recursive, matches, flags, forcetype) | ||||
| return (node for i, node in gen) | return (node for i, node in gen) | ||||
| def filter(self, *args, **kwargs): | |||||
| def filter(self, *args: Any, **kwargs: Any) -> list[Node]: | |||||
| """Return a list of nodes within our list matching certain conditions. | """Return a list of nodes within our list matching certain conditions. | ||||
| This is equivalent to calling :func:`list` on :meth:`ifilter`. | This is equivalent to calling :func:`list` on :meth:`ifilter`. | ||||
| @@ -566,13 +613,13 @@ class Wikicode(StringMixIn): | |||||
| def get_sections( | def get_sections( | ||||
| self, | self, | ||||
| levels=None, | |||||
| matches=None, | |||||
| flags=FLAGS, | |||||
| flat=False, | |||||
| include_lead=None, | |||||
| include_headings=True, | |||||
| ): | |||||
| levels: Optional[Iterable[int]] = None, | |||||
| matches: Union[Callable[[Node], bool], re.Pattern, str, None] = None, | |||||
| flags: int = FLAGS, | |||||
| flat: bool = False, | |||||
| include_lead: Optional[bool] = None, | |||||
| include_headings: bool = True, | |||||
| ) -> list["Wikicode"]: | |||||
| """Return a list of sections within the page. | """Return a list of sections within the page. | ||||
| Sections are returned as :class:`.Wikicode` objects with a shared node | Sections are returned as :class:`.Wikicode` objects with a shared node | ||||
| @@ -601,7 +648,7 @@ class Wikicode(StringMixIn): | |||||
| sections = [] # Tuples of (index_of_first_node, section) | sections = [] # Tuples of (index_of_first_node, section) | ||||
| # Tuples of (index, heading), where index and heading.level are both | # Tuples of (index, heading), where index and heading.level are both | ||||
| # monotonically increasing | # monotonically increasing | ||||
| open_headings = [] | |||||
| open_headings: list[tuple[int, Heading]] = [] | |||||
| # Add the lead section if appropriate: | # Add the lead section if appropriate: | ||||
| if include_lead or not (include_lead is not None or matches or levels): | if include_lead or not (include_lead is not None or matches or levels): | ||||
| @@ -638,7 +685,12 @@ class Wikicode(StringMixIn): | |||||
| # Ensure that earlier sections are earlier in the returned list: | # Ensure that earlier sections are earlier in the returned list: | ||||
| return [section for i, section in sorted(sections)] | return [section for i, section in sorted(sections)] | ||||
| def strip_code(self, normalize=True, collapse=True, keep_template_params=False): | |||||
| def strip_code( | |||||
| self, | |||||
| normalize: bool = True, | |||||
| collapse: bool = True, | |||||
| keep_template_params: bool = False, | |||||
| ) -> str: | |||||
| """Return a rendered string without unprintable code such as templates. | """Return a rendered string without unprintable code such as templates. | ||||
| The way a node is stripped is handled by the | The way a node is stripped is handled by the | ||||
| @@ -674,7 +726,7 @@ class Wikicode(StringMixIn): | |||||
| return stripped | return stripped | ||||
| return "".join(nodes) | return "".join(nodes) | ||||
| def get_tree(self): | |||||
| def get_tree(self) -> str: | |||||
| """Return a hierarchical tree representation of the object. | """Return a hierarchical tree representation of the object. | ||||
| The representation is a string makes the most sense printed. It is | The representation is a string makes the most sense printed. It is | ||||