Переглянути джерело

Add more type hints (#338)

* Fix formatting

* Add quick mypy config file

* Add type hints: mwparserfromhell.definitions

* Add type hints: mwparserfromhell.wikicode

* Add type hints: mwparserfromhell.nodes._base

* Add type hints: mwparserfromhell.nodes.argument

* Add type hints: mwparserfromhell.nodes.comment

* Add type hints: mwparserfromhell.nodes.external_link

* Add type hints: mwparserfromhell.nodes.heading

* Add type hints: mwparserfromhell.nodes.html_entity

* Add type hints: mwparserfromhell.nodes.tag

* Add type hints: mwparserfromhell.nodes.template

* Add type hints: mwparserfromhell.nodes.text

* Add type hints: mwparserfromhell.nodes.wikilink

* Add type hints: mwparserfromhell.nodes.extras.attribute

* Add type hints: mwparserfromhell.nodes.extras.parameter

* Fix typo in docstring

---------

Co-authored-by: Jakub Klinkovský <1289205+lahwaacz@users.noreply.github.com>
tags/v0.7.0
Jakub Klinkovský 1 рік тому
committed by GitHub
джерело
коміт
3e9da21a84
Не вдалося знайти GPG ключ що відповідає даному підпису Ідентифікатор GPG ключа: B5690EEEBB952194
15 змінених файлів з 533 додано та 343 видалено
  1. +9
    -0
      mypy.ini
  2. +6
    -6
      src/mwparserfromhell/definitions.py
  3. +14
    -4
      src/mwparserfromhell/nodes/_base.py
  4. +22
    -12
      src/mwparserfromhell/nodes/argument.py
  5. +6
    -4
      src/mwparserfromhell/nodes/comment.py
  6. +35
    -19
      src/mwparserfromhell/nodes/external_link.py
  7. +58
    -48
      src/mwparserfromhell/nodes/extras/attribute.py
  8. +21
    -16
      src/mwparserfromhell/nodes/extras/parameter.py
  9. +21
    -13
      src/mwparserfromhell/nodes/heading.py
  10. +47
    -38
      src/mwparserfromhell/nodes/html_entity.py
  11. +98
    -85
      src/mwparserfromhell/nodes/tag.py
  12. +62
    -36
      src/mwparserfromhell/nodes/template.py
  13. +17
    -7
      src/mwparserfromhell/nodes/text.py
  14. +22
    -12
      src/mwparserfromhell/nodes/wikilink.py
  15. +95
    -43
      src/mwparserfromhell/wikicode.py

+ 9
- 0
mypy.ini Переглянути файл

@@ -0,0 +1,9 @@
# Configuration file for mypy
# https://mypy.readthedocs.io/en/stable/config_file.html

[mypy]
disallow_incomplete_defs = true
;disallow_untyped_defs = True
warn_return_any = true
warn_unused_configs = true
warn_unused_ignores = true

+ 6
- 6
src/mwparserfromhell/definitions.py Переглянути файл

@@ -113,32 +113,32 @@ MARKUP_TO_HTML = {
}


def get_html_tag(markup):
def get_html_tag(markup: str) -> str:
"""Return the HTML tag associated with the given wiki-markup."""
return MARKUP_TO_HTML[markup]


def is_parsable(tag):
def is_parsable(tag: str) -> bool:
"""Return if the given *tag*'s contents should be passed to the parser."""
return tag.lower() not in PARSER_BLACKLIST


def is_visible(tag):
def is_visible(tag: str) -> bool:
"""Return whether or not the given *tag* contains visible text."""
return tag.lower() not in INVISIBLE_TAGS


def is_single(tag):
def is_single(tag: str) -> bool:
"""Return whether or not the given *tag* can exist without a close tag."""
return tag.lower() in SINGLE


def is_single_only(tag):
def is_single_only(tag: str) -> bool:
"""Return whether or not the given *tag* must exist without a close tag."""
return tag.lower() in SINGLE_ONLY


def is_scheme(scheme, slashes=True):
def is_scheme(scheme: str, slashes: bool = True) -> bool:
"""Return whether *scheme* is valid for external links."""
scheme = scheme.lower()
if slashes:


+ 14
- 4
src/mwparserfromhell/nodes/_base.py Переглянути файл

@@ -18,8 +18,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from typing import TYPE_CHECKING, Any, Generator, Optional, Callable

from ..string_mixin import StringMixIn

if TYPE_CHECKING:
from ..wikicode import Wikicode

__all__ = ["Node"]


@@ -37,16 +42,21 @@ class Node(StringMixIn):
of the node, if desired, for :meth:`~.Wikicode.get_tree`.
"""

def __str__(self):
def __str__(self) -> str:
raise NotImplementedError()

def __children__(self):
def __children__(self) -> Generator["Wikicode"]:
return
# pylint: disable=unreachable
yield # pragma: no cover (this is a generator that yields nothing)

def __strip__(self, **kwargs):
def __strip__(self, **kwargs: Any) -> Optional[str]:
return None

def __showtree__(self, write, get, mark):
def __showtree__(
self,
write: Callable[[str], None],
get: Callable[["Wikicode"], None],
mark: Callable[[], None],
) -> None:
write(str(self))

+ 22
- 12
src/mwparserfromhell/nodes/argument.py Переглянути файл

@@ -19,37 +19,47 @@
# SOFTWARE.


from typing import TYPE_CHECKING, Any, Generator, Optional, Callable

from ._base import Node
from ..utils import parse_anything

if TYPE_CHECKING:
from ..wikicode import Wikicode

__all__ = ["Argument"]


class Argument(Node):
"""Represents a template argument substitution, like ``{{{foo}}}``."""

def __init__(self, name, default=None):
def __init__(self, name: Any, default: Any = None):
super().__init__()
self.name = name
self.default = default

def __str__(self):
def __str__(self) -> str:
start = "{{{" + str(self.name)
if self.default is not None:
return start + "|" + str(self.default) + "}}}"
return start + "}}}"

def __children__(self):
def __children__(self) -> Generator["Wikicode"]:
yield self.name
if self.default is not None:
yield self.default

def __strip__(self, **kwargs):
def __strip__(self, **kwargs: Any) -> Optional[str]:
if self.default is not None:
return self.default.strip_code(**kwargs)
return None

def __showtree__(self, write, get, mark):
def __showtree__(
self,
write: Callable[[str], None],
get: Callable[["Wikicode"], None],
mark: Callable[[], None],
) -> None:
write("{{{")
get(self.name)
if self.default is not None:
@@ -59,12 +69,16 @@ class Argument(Node):
write("}}}")

@property
def name(self):
def name(self) -> "Wikicode":
"""The name of the argument to substitute."""
return self._name

@name.setter
def name(self, value: Any) -> None:
self._name = parse_anything(value)

@property
def default(self):
def default(self) -> Optional["Wikicode"]:
"""The default value to substitute if none is passed.

This will be ``None`` if the argument wasn't defined with one. The
@@ -74,12 +88,8 @@ class Argument(Node):
"""
return self._default

@name.setter
def name(self, value):
self._name = parse_anything(value)

@default.setter
def default(self, default):
def default(self, default: Any) -> None:
if default is None:
self._default = None
else:


+ 6
- 4
src/mwparserfromhell/nodes/comment.py Переглянути файл

@@ -19,6 +19,8 @@
# SOFTWARE.


from typing import Any

from ._base import Node

__all__ = ["Comment"]
@@ -27,18 +29,18 @@ __all__ = ["Comment"]
class Comment(Node):
"""Represents a hidden HTML comment, like ``<!-- foobar -->``."""

def __init__(self, contents):
def __init__(self, contents: str):
super().__init__()
self.contents = contents

def __str__(self):
def __str__(self) -> str:
return "<!--" + self.contents + "-->"

@property
def contents(self):
def contents(self) -> str:
"""The hidden text contained between ``<!--`` and ``-->``."""
return self._contents

@contents.setter
def contents(self, value):
def contents(self, value: Any) -> None:
self._contents = str(value)

+ 35
- 19
src/mwparserfromhell/nodes/external_link.py Переглянути файл

@@ -19,23 +19,34 @@
# SOFTWARE.


from typing import TYPE_CHECKING, Any, Optional, Generator, Callable

from ._base import Node
from ..utils import parse_anything

if TYPE_CHECKING:
from ..wikicode import Wikicode

__all__ = ["ExternalLink"]


class ExternalLink(Node):
"""Represents an external link, like ``[http://example.com/ Example]``."""

def __init__(self, url, title=None, brackets=True, suppress_space=False):
def __init__(
self,
url: Any,
title: Any = None,
brackets: bool = True,
suppress_space: bool = False,
):
super().__init__()
self.url = url
self.title = title
self.brackets = brackets
self.suppress_space = suppress_space

def __str__(self):
def __str__(self) -> str:
if self.brackets:
if self.title is not None:
if self.suppress_space is True:
@@ -44,19 +55,24 @@ class ExternalLink(Node):
return "[" + str(self.url) + "]"
return str(self.url)

def __children__(self):
def __children__(self) -> Generator["Wikicode"]:
yield self.url
if self.title is not None:
yield self.title

def __strip__(self, **kwargs):
def __strip__(self, **kwargs: Any) -> Optional[str]:
if self.brackets:
if self.title:
return self.title.strip_code(**kwargs)
return None
return self.url.strip_code(**kwargs)

def __showtree__(self, write, get, mark):
def __showtree__(
self,
write: Callable[[str], None],
get: Callable[["Wikicode"], None],
mark: Callable[[], None],
) -> None:
if self.brackets:
write("[")
get(self.url)
@@ -66,31 +82,31 @@ class ExternalLink(Node):
write("]")

@property
def url(self):
def url(self) -> "Wikicode":
"""The URL of the link target, as a :class:`.Wikicode` object."""
return self._url

@property
def title(self):
"""The link title (if given), as a :class:`.Wikicode` object."""
return self._title

@property
def brackets(self):
"""Whether to enclose the URL in brackets or display it straight."""
return self._brackets

@url.setter
def url(self, value):
def url(self, value: Any) -> None:
# pylint: disable=import-outside-toplevel
from ..parser import contexts

self._url = parse_anything(value, contexts.EXT_LINK_URI)

@property
def title(self) -> Optional["Wikicode"]:
"""The link title (if given), as a :class:`.Wikicode` object."""
return self._title

@title.setter
def title(self, value):
def title(self, value: Any) -> None:
self._title = None if value is None else parse_anything(value)

@property
def brackets(self) -> bool:
"""Whether to enclose the URL in brackets or display it straight."""
return self._brackets

@brackets.setter
def brackets(self, value):
def brackets(self, value: bool) -> None:
self._brackets = bool(value)

+ 58
- 48
src/mwparserfromhell/nodes/extras/attribute.py Переглянути файл

@@ -19,9 +19,14 @@
# SOFTWARE.


from typing import TYPE_CHECKING, Any, Optional

from ...string_mixin import StringMixIn
from ...utils import parse_anything

if TYPE_CHECKING:
from ...wikicode import Wikicode

__all__ = ["Attribute"]


@@ -35,23 +40,28 @@ class Attribute(StringMixIn):

def __init__(
self,
name,
value=None,
quotes='"',
pad_first=" ",
pad_before_eq="",
pad_after_eq="",
name: Any,
value: Any = None,
quotes: Optional[str] = '"',
pad_first: str = " ",
pad_before_eq: str = "",
pad_after_eq: str = "",
):
super().__init__()

self._pad_first: str
self._pad_before_eq: str
self._pad_after_eq: str

self.name = name
self._quotes = None
self._quotes: Optional[str] = None
self.value = value
self.quotes = quotes
self.pad_first = pad_first
self.pad_before_eq = pad_before_eq
self.pad_after_eq = pad_after_eq

def __str__(self):
def __str__(self) -> str:
result = self.pad_first + str(self.name) + self.pad_before_eq
if self.value is not None:
result += "=" + self.pad_after_eq
@@ -61,11 +71,11 @@ class Attribute(StringMixIn):
return result

@staticmethod
def _value_needs_quotes(val):
def _value_needs_quotes(value: "Wikicode") -> Optional[str]:
"""Return valid quotes for the given value, or None if unneeded."""
if not val:
if not value:
return None
val = "".join(str(node) for node in val.filter_text(recursive=False))
val = "".join(str(node) for node in value.filter_text(recursive=False))
if not any(char.isspace() for char in val):
return None
if "'" in val and '"' not in val:
@@ -74,7 +84,7 @@ class Attribute(StringMixIn):
return "'"
return "\"'" # Either acceptable, " preferred over '

def _set_padding(self, attr, value):
def _set_padding(self, attr: str, value: str) -> None:
"""Setter for the value of a padding attribute."""
if not value:
setattr(self, attr, "")
@@ -85,49 +95,29 @@ class Attribute(StringMixIn):
setattr(self, attr, value)

@staticmethod
def coerce_quotes(quotes):
def coerce_quotes(quotes: Any) -> Optional[str]:
"""Coerce a quote type into an acceptable value, or raise an error."""
orig, quotes = quotes, str(quotes) if quotes else None
if quotes not in [None, '"', "'"]:
raise ValueError("{!r} is not a valid quote type".format(orig))
return quotes
coerced_quotes = str(quotes) if quotes else None
if coerced_quotes not in [None, '"', "'"]:
raise ValueError("{!r} is not a valid quote type".format(quotes))
return coerced_quotes

@property
def name(self):
def name(self) -> "Wikicode":
"""The name of the attribute as a :class:`.Wikicode` object."""
return self._name

@name.setter
def name(self, value: Any) -> None:
self._name = parse_anything(value)

@property
def value(self):
def value(self) -> Optional["Wikicode"]:
"""The value of the attribute as a :class:`.Wikicode` object."""
return self._value

@property
def quotes(self):
"""How to enclose the attribute value. ``"``, ``'``, or ``None``."""
return self._quotes

@property
def pad_first(self):
"""Spacing to insert right before the attribute."""
return self._pad_first

@property
def pad_before_eq(self):
"""Spacing to insert right before the equal sign."""
return self._pad_before_eq

@property
def pad_after_eq(self):
"""Spacing to insert right after the equal sign."""
return self._pad_after_eq

@name.setter
def name(self, value):
self._name = parse_anything(value)

@value.setter
def value(self, newval):
def value(self, newval: Any) -> None:
if newval is None:
self._value = None
else:
@@ -137,21 +127,41 @@ class Attribute(StringMixIn):
self._quotes = quotes[0]
self._value = code

@property
def quotes(self) -> Optional[str]:
"""How to enclose the attribute value. ``"``, ``'``, or ``None``."""
return self._quotes

@quotes.setter
def quotes(self, value):
def quotes(self, value: Any) -> None:
value = self.coerce_quotes(value)
if not value and self._value_needs_quotes(self.value):
raise ValueError("attribute value requires quotes")
self._quotes = value

@property
def pad_first(self) -> str:
"""Spacing to insert right before the attribute."""
return self._pad_first

@pad_first.setter
def pad_first(self, value):
def pad_first(self, value: str) -> None:
self._set_padding("_pad_first", value)

@property
def pad_before_eq(self) -> str:
"""Spacing to insert right before the equal sign."""
return self._pad_before_eq

@pad_before_eq.setter
def pad_before_eq(self, value):
def pad_before_eq(self, value: str) -> None:
self._set_padding("_pad_before_eq", value)

@property
def pad_after_eq(self) -> str:
"""Spacing to insert right after the equal sign."""
return self._pad_after_eq

@pad_after_eq.setter
def pad_after_eq(self, value):
def pad_after_eq(self, value: str) -> None:
self._set_padding("_pad_after_eq", value)

+ 21
- 16
src/mwparserfromhell/nodes/extras/parameter.py Переглянути файл

@@ -18,16 +18,21 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from typing import TYPE_CHECKING, Any, Union

import re

from ...string_mixin import StringMixIn
from ...utils import parse_anything

if TYPE_CHECKING:
from ...wikicode import Wikicode

__all__ = ["Parameter"]


class Parameter(StringMixIn):
"""Represents a paramater of a template.
"""Represents a parameter of a template.

For example, the template ``{{foo|bar|spam=eggs}}`` contains two
Parameters: one whose name is ``"1"``, value is ``"bar"``, and ``showkey``
@@ -35,47 +40,47 @@ class Parameter(StringMixIn):
``showkey`` is ``True``.
"""

def __init__(self, name, value, showkey=True):
def __init__(self, name: Any, value: Any, showkey: bool = True) -> None:
super().__init__()
self.name = name
self.value = value
self.showkey = showkey

def __str__(self):
def __str__(self) -> str:
if self.showkey:
return str(self.name) + "=" + str(self.value)
return str(self.value)

@staticmethod
def can_hide_key(key):
def can_hide_key(key: Any) -> Union[re.Match, None]:
"""Return whether or not the given key can be hidden."""
return re.match(r"[1-9][0-9]*$", str(key).strip())

@property
def name(self):
def name(self) -> "Wikicode":
"""The name of the parameter as a :class:`.Wikicode` object."""
return self._name

@name.setter
def name(self, newval: Any) -> None:
self._name = parse_anything(newval)

@property
def value(self):
def value(self) -> "Wikicode":
"""The value of the parameter as a :class:`.Wikicode` object."""
return self._value

@value.setter
def value(self, newval: Any) -> None:
self._value = parse_anything(newval)

@property
def showkey(self):
def showkey(self) -> bool:
"""Whether to show the parameter's key (i.e., its "name")."""
return self._showkey

@name.setter
def name(self, newval):
self._name = parse_anything(newval)

@value.setter
def value(self, newval):
self._value = parse_anything(newval)

@showkey.setter
def showkey(self, newval):
def showkey(self, newval: Any) -> None:
newval = bool(newval)
if not newval and not self.can_hide_key(self.name):
raise ValueError("parameter key {!r} cannot be hidden".format(self.name))


+ 21
- 13
src/mwparserfromhell/nodes/heading.py Переглянути файл

@@ -19,50 +19,58 @@
# SOFTWARE.


from typing import TYPE_CHECKING, Any, Generator, Callable, Optional

from ._base import Node
from ..utils import parse_anything

__all__ = ["Heading"]
if TYPE_CHECKING:
from ..wikicode import Wikicode


class Heading(Node):
"""Represents a section heading in wikicode, like ``== Foo ==``."""

def __init__(self, title, level):
def __init__(self, title: Any, level: int):
super().__init__()
self.title = title
self.level = level

def __str__(self):
def __str__(self) -> str:
return ("=" * self.level) + str(self.title) + ("=" * self.level)

def __children__(self):
def __children__(self) -> Generator["Wikicode", None, None]:
yield self.title

def __strip__(self, **kwargs):
def __strip__(self, **kwargs: Any) -> Optional[str]:
return self.title.strip_code(**kwargs)

def __showtree__(self, write, get, mark):
def __showtree__(
self,
write: Callable[[str], None],
get: Callable[["Wikicode"], None],
mark: Callable[[], None],
) -> None:
write("=" * self.level)
get(self.title)
write("=" * self.level)

@property
def title(self):
def title(self) -> "Wikicode":
"""The title of the heading, as a :class:`.Wikicode` object."""
return self._title

@title.setter
def title(self, value: Any) -> None:
self._title = parse_anything(value)

@property
def level(self):
def level(self) -> int:
"""The heading level, as an integer between 1 and 6, inclusive."""
return self._level

@title.setter
def title(self, value):
self._title = parse_anything(value)

@level.setter
def level(self, value):
def level(self, value: int) -> None:
value = int(value)
if value < 1 or value > 6:
raise ValueError(value)


+ 47
- 38
src/mwparserfromhell/nodes/html_entity.py Переглянути файл

@@ -18,6 +18,7 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from typing import Any, Optional
import html.entities as htmlentities

from ._base import Node
@@ -28,9 +29,15 @@ __all__ = ["HTMLEntity"]
class HTMLEntity(Node):
"""Represents an HTML entity, like ``&nbsp;``, either named or unnamed."""

def __init__(self, value, named=None, hexadecimal=False, hex_char="x"):
def __init__(
self,
value: Any,
named: Optional[bool] = None,
hexadecimal: bool = False,
hex_char: str = "x",
):
super().__init__()
self._value = value
self._value = str(value)
if named is None: # Try to guess whether or not the entity is named
try:
int(value)
@@ -49,50 +56,25 @@ class HTMLEntity(Node):
self._hexadecimal = hexadecimal
self._hex_char = hex_char

def __str__(self):
def __str__(self) -> str:
if self.named:
return "&{};".format(self.value)
if self.hexadecimal:
return "&#{}{};".format(self.hex_char, self.value)
return "&#{};".format(self.value)

def __strip__(self, **kwargs):
def __strip__(self, **kwargs: Any) -> Optional[str]:
if kwargs.get("normalize"):
return self.normalize()
return self
return str(self)

@property
def value(self):
def value(self) -> str:
"""The string value of the HTML entity."""
return self._value

@property
def named(self):
"""Whether the entity is a string name for a codepoint or an integer.

For example, ``&Sigma;``, ``&#931;``, and ``&#x3a3;`` refer to the same
character, but only the first is "named", while the others are integer
representations of the codepoint.
"""
return self._named

@property
def hexadecimal(self):
"""If unnamed, this is whether the value is hexadecimal or decimal."""
return self._hexadecimal

@property
def hex_char(self):
"""If the value is hexadecimal, this is the letter denoting that.

For example, the hex_char of ``"&#x1234;"`` is ``"x"``, whereas the
hex_char of ``"&#X1234;"`` is ``"X"``. Lowercase and uppercase ``x``
are the only values supported.
"""
return self._hex_char

@value.setter
def value(self, newval):
def value(self, newval: Any) -> None:
newval = str(newval)
try:
int(newval)
@@ -122,36 +104,63 @@ class HTMLEntity(Node):
self._named = False
self._value = newval

@property
def named(self) -> bool:
"""Whether the entity is a string name for a codepoint or an integer.

For example, ``&Sigma;``, ``&#931;``, and ``&#x3a3;`` refer to the same
character, but only the first is "named", while the others are integer
representations of the codepoint.
"""
return self._named

@named.setter
def named(self, newval):
def named(self, newval: bool) -> None:
newval = bool(newval)
if newval and self.value not in htmlentities.entitydefs:
raise ValueError("entity value {!r} is not a valid name".format(self.value))

if not newval:
try:
int(self.value, 16)
except ValueError as exc:
raise ValueError(
"current entity value {!r} is not a valid "
"Unicode codepoint".format(self.value)
"current entity value {!r} is not a valid Unicode codepoint".format(
self.value
)
) from exc
self._named = newval

@property
def hexadecimal(self) -> bool:
"""If unnamed, this is whether the value is hexadecimal or decimal."""
return self._hexadecimal

@hexadecimal.setter
def hexadecimal(self, newval):
def hexadecimal(self, newval: bool) -> None:
newval = bool(newval)
if newval and self.named:
raise ValueError("a named entity cannot be hexadecimal")
self._hexadecimal = newval

@property
def hex_char(self) -> str:
"""If the value is hexadecimal, this is the letter denoting that.

For example, the hex_char of ``"&#x1234;"`` is ``"x"``, whereas the
hex_char of ``"&#X1234;"`` is ``"X"``. Lowercase and uppercase ``x``
are the only values supported.
"""
return self._hex_char

@hex_char.setter
def hex_char(self, newval):
def hex_char(self, newval: str) -> None:
newval = str(newval)
if newval not in ("x", "X"):
raise ValueError(newval)
self._hex_char = newval

def normalize(self):
def normalize(self) -> str:
"""Return the unicode character represented by the HTML entity."""
if self.named:
return chr(htmlentities.name2codepoint[self.value])


+ 98
- 85
src/mwparserfromhell/nodes/tag.py Переглянути файл

@@ -19,11 +19,16 @@
# SOFTWARE.


from typing import TYPE_CHECKING, Any, Callable, Optional, List, Generator

from ._base import Node
from .extras import Attribute
from ..definitions import is_visible
from ..utils import parse_anything

if TYPE_CHECKING:
from ..wikicode import Wikicode

__all__ = ["Tag"]


@@ -32,19 +37,22 @@ class Tag(Node):

def __init__(
self,
tag,
contents=None,
attrs=None,
wiki_markup=None,
self_closing=False,
invalid=False,
implicit=False,
padding="",
closing_tag=None,
wiki_style_separator=None,
closing_wiki_markup=None,
tag: Any,
contents: Any = None,
attrs: Optional[List[Attribute]] = None,
wiki_markup: Optional[str] = None,
self_closing: bool = False,
invalid: bool = False,
implicit: bool = False,
padding: str = "",
closing_tag: Optional["Wikicode"] = None,
wiki_style_separator: Optional[str] = None,
closing_wiki_markup: Optional[str] = None,
):
super().__init__()
self._attrs: List[Attribute]
self._closing_wiki_markup: Optional[str]

self.tag = tag
self.contents = contents
self._attrs = attrs if attrs else []
@@ -60,7 +68,7 @@ class Tag(Node):
if closing_wiki_markup is not None:
self.closing_wiki_markup = closing_wiki_markup

def __str__(self):
def __str__(self) -> str:
if self.wiki_markup:
if self.attributes:
attrs = "".join([str(attr) for attr in self.attributes])
@@ -90,7 +98,7 @@ class Tag(Node):
result += "</" + str(self.closing_tag) + ">"
return result

def __children__(self):
def __children__(self) -> Generator["Wikicode"]:
if not self.wiki_markup:
yield self.tag
for attr in self.attributes:
@@ -102,12 +110,17 @@ class Tag(Node):
if not self.wiki_markup and self.closing_tag:
yield self.closing_tag

def __strip__(self, **kwargs):
if self.contents and is_visible(self.tag):
def __strip__(self, **kwargs: Any) -> Optional[str]:
if self.contents and is_visible(str(self.tag)):
return self.contents.strip_code(**kwargs)
return None

def __showtree__(self, write, get, mark):
def __showtree__(
self,
write: Callable[[str], None],
get: Callable[["Wikicode"], None],
mark: Callable[[], None],
) -> None:
write("</" if self.invalid else "<")
get(self.tag)
for attr in self.attributes:
@@ -127,17 +140,25 @@ class Tag(Node):
write(">")

@property
def tag(self):
def tag(self) -> "Wikicode":
"""The tag itself, as a :class:`.Wikicode` object."""
return self._tag

@tag.setter
def tag(self, value: Any) -> None:
self._tag = self._closing_tag = parse_anything(value)

@property
def contents(self):
def contents(self) -> "Wikicode":
"""The contents of the tag, as a :class:`.Wikicode` object."""
return self._contents

@contents.setter
def contents(self, value: Any) -> None:
self._contents = parse_anything(value)

@property
def attributes(self):
def attributes(self) -> List[Attribute]:
"""The list of attributes affecting the tag.

Each attribute is an instance of :class:`.Attribute`.
@@ -145,7 +166,7 @@ class Tag(Node):
return self._attrs

@property
def wiki_markup(self):
def wiki_markup(self) -> Optional[str]:
"""The wikified version of a tag to show instead of HTML.

If set to a value, this will be displayed instead of the brackets.
@@ -154,13 +175,23 @@ class Tag(Node):
"""
return self._wiki_markup

@wiki_markup.setter
def wiki_markup(self, value: Optional[str]) -> None:
self._wiki_markup = str(value) if value else None
if not value or not self.closing_wiki_markup:
self._closing_wiki_markup = self._wiki_markup

@property
def self_closing(self):
def self_closing(self) -> bool:
"""Whether the tag is self-closing with no content (like ``<br/>``)."""
return self._self_closing

@self_closing.setter
def self_closing(self, value: bool) -> None:
self._self_closing = bool(value)

@property
def invalid(self):
def invalid(self) -> bool:
"""Whether the tag starts with a backslash after the opening bracket.

This makes the tag look like a lone close tag. It is technically
@@ -170,8 +201,12 @@ class Tag(Node):
"""
return self._invalid

@invalid.setter
def invalid(self, value: bool) -> None:
self._invalid = bool(value)

@property
def implicit(self):
def implicit(self) -> bool:
"""Whether the tag is implicitly self-closing, with no ending slash.

This is only possible for specific "single" tags like ``<br>`` and
@@ -180,13 +215,27 @@ class Tag(Node):
"""
return self._implicit

@implicit.setter
def implicit(self, value: bool) -> None:
self._implicit = bool(value)

@property
def padding(self):
def padding(self) -> str:
"""Spacing to insert before the first closing ``>``."""
return self._padding

@padding.setter
def padding(self, value: str) -> None:
if not value:
self._padding = ""
else:
value = str(value)
if not value.isspace():
raise ValueError("padding must be entirely whitespace")
self._padding = value

@property
def closing_tag(self):
def closing_tag(self) -> "Wikicode":
"""The closing tag, as a :class:`.Wikicode` object.

This will usually equal :attr:`tag`, unless there is additional
@@ -194,76 +243,40 @@ class Tag(Node):
"""
return self._closing_tag

@closing_tag.setter
def closing_tag(self, value: Any) -> None:
self._closing_tag = parse_anything(value)

@property
def wiki_style_separator(self):
def wiki_style_separator(self) -> Optional[str]:
"""The separator between the padding and content in a wiki markup tag.

Essentially the wiki equivalent of the TagCloseOpen.
"""
return self._wiki_style_separator

@wiki_style_separator.setter
def wiki_style_separator(self, value: Optional[str]) -> None:
self._wiki_style_separator = str(value) if value else None

@property
def closing_wiki_markup(self):
def closing_wiki_markup(self) -> Optional[str]:
"""The wikified version of the closing tag to show instead of HTML.

If set to a value, this will be displayed instead of the close tag
brackets. If tag is :attr:`self_closing` is ``True`` then this is not
displayed. If :attr:`wiki_markup` is set and this has not been set, this
is set to the value of :attr:`wiki_markup`. If this has been set and
brackets. If the tag is :attr:`self_closing`, this is not displayed.
If :attr:`wiki_markup` is set and this has not been set, this is set
to the value of :attr:`wiki_markup`. If this has been set and
:attr:`wiki_markup` is set to a ``False`` value, this is set to
``None``.
"""
return self._closing_wiki_markup

@tag.setter
def tag(self, value):
self._tag = self._closing_tag = parse_anything(value)

@contents.setter
def contents(self, value):
self._contents = parse_anything(value)

@wiki_markup.setter
def wiki_markup(self, value):
self._wiki_markup = str(value) if value else None
if not value or not self.closing_wiki_markup:
self._closing_wiki_markup = self._wiki_markup

@self_closing.setter
def self_closing(self, value):
self._self_closing = bool(value)

@invalid.setter
def invalid(self, value):
self._invalid = bool(value)

@implicit.setter
def implicit(self, value):
self._implicit = bool(value)

@padding.setter
def padding(self, value):
if not value:
self._padding = ""
else:
value = str(value)
if not value.isspace():
raise ValueError("padding must be entirely whitespace")
self._padding = value

@closing_tag.setter
def closing_tag(self, value):
self._closing_tag = parse_anything(value)

@wiki_style_separator.setter
def wiki_style_separator(self, value):
self._wiki_style_separator = str(value) if value else None

@closing_wiki_markup.setter
def closing_wiki_markup(self, value):
def closing_wiki_markup(self, value: Optional[str]) -> None:
self._closing_wiki_markup = str(value) if value else None

def has(self, name):
def has(self, name: str) -> bool:
"""Return whether any attribute in the tag has the given *name*.

Note that a tag may have multiple attributes with the same name, but
@@ -274,7 +287,7 @@ class Tag(Node):
return True
return False

def get(self, name):
def get(self, name: str) -> Attribute:
"""Get the attribute with the given *name*.

The returned object is a :class:`.Attribute` instance. Raises
@@ -289,13 +302,13 @@ class Tag(Node):

def add(
self,
name,
value=None,
quotes='"',
pad_first=" ",
pad_before_eq="",
pad_after_eq="",
):
name: Any,
value: Any = None,
quotes: Optional[str] = '"',
pad_first: str = " ",
pad_before_eq: str = "",
pad_after_eq: str = "",
) -> Attribute:
"""Add an attribute with the given *name* and *value*.

*name* and *value* can be anything parsable by
@@ -318,7 +331,7 @@ class Tag(Node):
self.attributes.append(attr)
return attr

def remove(self, name):
def remove(self, name: str) -> None:
"""Remove all attributes with the given *name*.

Raises :exc:`ValueError` if none were found.


+ 62
- 36
src/mwparserfromhell/nodes/template.py Переглянути файл

@@ -19,6 +19,18 @@
# SOFTWARE.
from collections import defaultdict
import re
from typing import (
TYPE_CHECKING,
Any,
Optional,
Union,
List,
Dict,
Callable,
Generator,
Mapping,
cast,
)

from ._base import Node
from .html_entity import HTMLEntity
@@ -26,43 +38,49 @@ from .text import Text
from .extras import Parameter
from ..utils import parse_anything

if TYPE_CHECKING:
from ..wikicode import Wikicode

__all__ = ["Template"]

FLAGS = re.DOTALL | re.UNICODE
# Used to allow None as a valid fallback value
_UNSET = object()


class Template(Node):
"""Represents a template in wikicode, like ``{{foo}}``."""

def __init__(self, name, params=None):
def __init__(self, name: Any, params: Optional[List[Parameter]] = None):
super().__init__()
self.name = name
if params:
self._params = params
else:
self._params = []
self._params: List[Parameter] = params or []

def __str__(self):
def __str__(self) -> str:
if self.params:
params = "|".join([str(param) for param in self.params])
return "{{" + str(self.name) + "|" + params + "}}"
return "{{" + str(self.name) + "}}"

def __children__(self):
def __children__(self) -> Generator["Wikicode"]:
yield self.name
for param in self.params:
if param.showkey:
yield param.name
yield param.value

def __strip__(self, **kwargs):
def __strip__(self, **kwargs: Any) -> Optional[str]:
if kwargs.get("keep_template_params"):
parts = [param.value.strip_code(**kwargs) for param in self.params]
return " ".join(part for part in parts if part)
return None

def __showtree__(self, write, get, mark):
def __showtree__(
self,
write: Callable[[str], None],
get: Callable[["Wikicode"], None],
mark: Callable[[], None],
) -> None:
write("{{")
get(self.name)
for param in self.params:
@@ -75,7 +93,7 @@ class Template(Node):
write("}}")

@staticmethod
def _surface_escape(code, char):
def _surface_escape(code: "Wikicode", char: str) -> None:
"""Return *code* with *char* escaped as an HTML entity.

The main use of this is to escape pipes (``|``) or equal signs (``=``)
@@ -88,7 +106,7 @@ class Template(Node):
code.replace(node, node.replace(char, replacement), False)

@staticmethod
def _select_theory(theories):
def _select_theory(theories: Dict[str, int]) -> Optional[str]:
"""Return the most likely spacing convention given different options.

Given a dictionary of convention options as keys and their occurrence
@@ -104,7 +122,7 @@ class Template(Node):
return None

@staticmethod
def _blank_param_value(value):
def _blank_param_value(value: "Wikicode") -> None:
"""Remove the content from *value* while keeping its whitespace.

Replace *value*\\ 's nodes with two text nodes, the first containing
@@ -119,15 +137,17 @@ class Template(Node):
before, after = match.group(1), match.group(2)
value.nodes = [Text(before), Text(after)]

def _get_spacing_conventions(self, use_names):
def _get_spacing_conventions(
self, use_names: bool
) -> tuple[Optional[str], Optional[str]]:
"""Try to determine the whitespace conventions for parameters.

This will examine the existing parameters and use
:meth:`_select_theory` to determine if there are any preferred styles
for how much whitespace to put before or after the value.
"""
before_theories = defaultdict(lambda: 0)
after_theories = defaultdict(lambda: 0)
before_theories: defaultdict[str, int] = defaultdict(lambda: 0)
after_theories: defaultdict[str, int] = defaultdict(lambda: 0)
for param in self.params:
if not param.showkey:
continue
@@ -149,14 +169,14 @@ class Template(Node):
after = self._select_theory(after_theories)
return before, after

def _fix_dependendent_params(self, i):
def _fix_dependendent_params(self, i: int) -> None:
"""Unhide keys if necessary after removing the param at index *i*."""
if not self.params[i].showkey:
for param in self.params[i + 1 :]:
if not param.showkey:
param.showkey = True

def _remove_exact(self, needle, keep_field):
def _remove_exact(self, needle: Parameter, keep_field: bool) -> None:
"""Remove a specific parameter, *needle*, from the template."""
for i, param in enumerate(self.params):
if param is needle:
@@ -168,7 +188,7 @@ class Template(Node):
return
raise ValueError(needle)

def _should_remove(self, i, name):
def _should_remove(self, i: int, name: str) -> bool:
"""Look ahead for a parameter with the same name, but hidden.

If one exists, we should remove the given one rather than blanking it.
@@ -182,20 +202,20 @@ class Template(Node):
return False

@property
def name(self):
def name(self) -> "Wikicode":
"""The name of the template, as a :class:`.Wikicode` object."""
return self._name

@name.setter
def name(self, value: Any) -> None:
self._name = parse_anything(value)

@property
def params(self):
def params(self) -> List[Parameter]:
"""The list of parameters contained within the template."""
return self._params

@name.setter
def name(self, value):
self._name = parse_anything(value)

def has(self, name, ignore_empty=False):
def has(self, name: Union[str, Any], ignore_empty: bool = False) -> bool:
"""Return ``True`` if any parameter in the template is named *name*.

With *ignore_empty*, ``False`` will be returned even if the template
@@ -211,11 +231,11 @@ class Template(Node):
return True
return False

def has_param(self, name, ignore_empty=False):
def has_param(self, name: Union[str, Any], ignore_empty: bool = False) -> bool:
"""Alias for :meth:`has`."""
return self.has(name, ignore_empty)

def get(self, name, default=_UNSET):
def get(self, name: Union[str, Any], default: Any = _UNSET) -> Union[Parameter, Any]:
"""Get the parameter whose name is *name*.

The returned object is a :class:`.Parameter` instance. Raises
@@ -232,12 +252,18 @@ class Template(Node):
raise ValueError(name)
return default

def __getitem__(self, name):
def __getitem__(self, name: Union[str, Any]) -> Parameter:
return self.get(name)

def add(
self, name, value, showkey=None, before=None, after=None, preserve_spacing=True
):
self,
name: Any,
value: Any,
showkey: Optional[bool] = None,
before: Optional[Union[Parameter, str]] = None,
after: Optional[Union[Parameter, str]] = None,
preserve_spacing: bool = True,
) -> Parameter:
"""Add a parameter to the template with a given *name* and *value*.

*name* and *value* can be anything parsable by
@@ -328,21 +354,21 @@ class Template(Node):
self.params.append(param)
return param

def update(self, params, **kwargs):
def update(self, params: Mapping[Any, Any], **kwargs: Any) -> None:
"""Update the template with multiple parameters at once.
Args:
params: A dictionary mapping parameter names to values.
params: A dictionary mapping parameter names to values.
**kwargs: Optional arguments that will be applied to all parameters,
matching the same arguments in :meth:`add` (showkey, before,
matching the same arguments in :meth:`add` (showkey, before,
after, preserve_spacing)
"""
for name, value in params.items():
self.add(name, value, **kwargs)

def __setitem__(self, name, value):
def __setitem__(self, name: Any, value: Any) -> Parameter:
return self.add(name, value)

def remove(self, param, keep_field=False):
def remove(self, param: Union[Parameter, str], keep_field: bool = False) -> None:
"""Remove a parameter from the template, identified by *param*.

If *param* is a :class:`.Parameter` object, it will be matched exactly,
@@ -389,5 +415,5 @@ class Template(Node):
for i in reversed(to_remove):
self.params.pop(i)

def __delitem__(self, param):
def __delitem__(self, param: Union[Parameter, str]) -> None:
return self.remove(param)

+ 17
- 7
src/mwparserfromhell/nodes/text.py Переглянути файл

@@ -19,32 +19,42 @@
# SOFTWARE.


from typing import TYPE_CHECKING, Any, Callable

from ._base import Node

if TYPE_CHECKING:
from ..wikicode import Wikicode

__all__ = ["Text"]


class Text(Node):
"""Represents ordinary, unformatted text with no special properties."""

def __init__(self, value):
def __init__(self, value: Any):
super().__init__()
self.value = value

def __str__(self):
def __str__(self) -> str:
return self.value

def __strip__(self, **kwargs):
return self
def __strip__(self, **kwargs: Any) -> str:
return str(self)

def __showtree__(self, write, get, mark):
def __showtree__(
self,
write: Callable[[str], None],
get: Callable[["Wikicode"], None],
mark: Callable[[], None],
) -> None:
write(str(self).encode("unicode_escape").decode("utf8"))

@property
def value(self):
def value(self) -> str:
"""The actual text itself."""
return self._value

@value.setter
def value(self, newval):
def value(self, newval: Any) -> None:
self._value = str(newval)

+ 22
- 12
src/mwparserfromhell/nodes/wikilink.py Переглянути файл

@@ -19,36 +19,46 @@
# SOFTWARE.


from typing import TYPE_CHECKING, Any, Optional, Generator, Callable

from ._base import Node
from ..utils import parse_anything

if TYPE_CHECKING:
from ..wikicode import Wikicode

__all__ = ["Wikilink"]


class Wikilink(Node):
"""Represents an internal wikilink, like ``[[Foo|Bar]]``."""

def __init__(self, title, text=None):
def __init__(self, title: Any, text: Any = None):
super().__init__()
self.title = title
self.text = text

def __str__(self):
def __str__(self) -> str:
if self.text is not None:
return "[[" + str(self.title) + "|" + str(self.text) + "]]"
return "[[" + str(self.title) + "]]"

def __children__(self):
def __children__(self) -> Generator["Wikicode", None, None]:
yield self.title
if self.text is not None:
yield self.text

def __strip__(self, **kwargs):
def __strip__(self, **kwargs: Any) -> Optional[str]:
if self.text is not None:
return self.text.strip_code(**kwargs)
return self.title.strip_code(**kwargs)

def __showtree__(self, write, get, mark):
def __showtree__(
self,
write: Callable[[str], None],
get: Callable[["Wikicode"], None],
mark: Callable[[], None],
) -> None:
write("[[")
get(self.title)
if self.text is not None:
@@ -58,21 +68,21 @@ class Wikilink(Node):
write("]]")

@property
def title(self):
def title(self) -> "Wikicode":
"""The title of the linked page, as a :class:`.Wikicode` object."""
return self._title

@title.setter
def title(self, value: Any) -> None:
self._title = parse_anything(value)

@property
def text(self):
def text(self) -> Optional["Wikicode"]:
"""The text to display (if any), as a :class:`.Wikicode` object."""
return self._text

@title.setter
def title(self, value):
self._title = parse_anything(value)

@text.setter
def text(self, value):
def text(self, value: Any) -> None:
if value is None:
self._text = None
else:


+ 95
- 43
src/mwparserfromhell/wikicode.py Переглянути файл

@@ -20,6 +20,7 @@

import re
from itertools import chain
from typing import Any, Callable, Generator, Iterable, Optional, Union, cast, overload

from .nodes import (
Argument,
@@ -55,15 +56,20 @@ class Wikicode(StringMixIn):

RECURSE_OTHERS = 2

def __init__(self, nodes):
def __init__(self, nodes: list[Node]):
super().__init__()
self._nodes = nodes

def __str__(self):
def __str__(self) -> str:
return "".join([str(node) for node in self.nodes])

@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*."""
yield (parent, node) if contexts else node
if restrict and isinstance(node, restrict):
@@ -74,14 +80,19 @@ class Wikicode(StringMixIn):
yield from sub

@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*."""
nodes = [str(node) for node in code.get(index)]
substring = "".join(nodes).replace(old, new)
code.nodes[index] = parse_anything(substring).nodes

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

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

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.

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.
"""
match = self._build_matcher(matches, flags)
inodes: Iterable[tuple[int, Node]]
if recursive:
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):
yield (i, ch)
yield (i, cast(Node, ch))

inodes = chain(*(getter(i, n) for i, n in enumerate(self.nodes)))
else:
@@ -119,7 +135,7 @@ class Wikicode(StringMixIn):
if (not forcetype or isinstance(node, forcetype)) and match(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."""

def deref(nodes):
@@ -140,7 +156,9 @@ class Wikicode(StringMixIn):
todo += list(node.__children__())
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.

*obj* can be either a :class:`.Node` or a :class:`.Wikicode` object. If
@@ -171,7 +189,9 @@ class Wikicode(StringMixIn):

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.

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:
raise ValueError(obj)
results = []
contexts = [self]
contexts: list["Wikicode"] = [self]
while contexts:
context = contexts.pop()
i = len(context.nodes) - 1
@@ -215,7 +235,9 @@ class Wikicode(StringMixIn):
results.append((False, self, slice(0, len(self.nodes))))
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.

The method that builds the actual tree is ``__showtree__`` of ``Node``
@@ -227,7 +249,7 @@ class Wikicode(StringMixIn):
the starting indentation.
"""

def write(*args):
def write(*args: str) -> None:
"""Write a new line following the proper indentation rules."""
if lines and lines[-1] is marker: # Continue from the last line
lines.pop() # Remove the marker
@@ -275,7 +297,7 @@ class Wikicode(StringMixIn):
setattr(cls, "filter_" + name, filt)

@property
def nodes(self):
def nodes(self) -> list[Node]:
"""A list of :class:`.Node` objects.

This is the internal data actually stored within a :class:`.Wikicode`
@@ -284,16 +306,22 @@ class Wikicode(StringMixIn):
return self._nodes

@nodes.setter
def nodes(self, value):
def nodes(self, value: Union[list[Node], Any]) -> None:
if not isinstance(value, list):
value = parse_anything(value).nodes
self._nodes = value

@overload
def get(self, index: int) -> Node: ...

@overload
def get(self, index: slice) -> list[Node]: ...

def get(self, index):
"""Return the *index*\\ th node within the list of nodes."""
return self.nodes[index]

def set(self, index, value):
def set(self, index: int, value: Any) -> None:
"""Set the ``Node`` at *index* to *value*.

Raises :exc:`IndexError` if *index* is out of range, or
@@ -311,7 +339,7 @@ class Wikicode(StringMixIn):
else:
self.nodes.pop(index)

def contains(self, obj):
def contains(self, obj: Union[Node, "Wikicode", str]) -> bool:
"""Return whether this Wikicode object contains *obj*.

If *obj* is a :class:`.Node` or :class:`.Wikicode` object, then we
@@ -326,7 +354,7 @@ class Wikicode(StringMixIn):
return False
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.

Raises :exc:`ValueError` if *obj* is not found. If *recursive* is
@@ -345,7 +373,7 @@ class Wikicode(StringMixIn):
return i
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*.

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

def _get_ancestors(code, needle):
def _get_ancestors(code: "Wikicode", needle: Node) -> Union[list[Node], None]:
for node in code.nodes:
if node is needle:
return []
@@ -382,7 +410,7 @@ class Wikicode(StringMixIn):
raise ValueError(obj)
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*.

This function is equivalent to calling :meth:`.get_ancestors` and
@@ -393,7 +421,7 @@ class Wikicode(StringMixIn):
ancestors = self.get_ancestors(obj)
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.

*value* can be anything parsable by :func:`.parse_anything`, which
@@ -403,7 +431,9 @@ class Wikicode(StringMixIn):
for node in reversed(nodes):
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*.

*obj* can be either a string, a :class:`.Node`, or another
@@ -426,7 +456,9 @@ class Wikicode(StringMixIn):
obj = str(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*.

*obj* can be either a string, a :class:`.Node`, or another
@@ -449,7 +481,9 @@ class Wikicode(StringMixIn):
obj = str(obj)
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*.

*obj* can be either a string, a :class:`.Node`, or another
@@ -475,7 +509,7 @@ class Wikicode(StringMixIn):
else:
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.

*value* can be anything parsable by :func:`.parse_anything`.
@@ -484,7 +518,7 @@ class Wikicode(StringMixIn):
for node in nodes:
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.

*obj* can be either a string, a :class:`.Node`, or another
@@ -508,7 +542,12 @@ class Wikicode(StringMixIn):
else:
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.

*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
``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())

if isinstance(other, (str, bytes, Wikicode, Node)):
@@ -530,7 +571,13 @@ class Wikicode(StringMixIn):
return True
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.

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

This is equivalent to calling :func:`list` on :meth:`ifilter`.
@@ -566,13 +613,13 @@ class Wikicode(StringMixIn):

def get_sections(
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.

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)
# Tuples of (index, heading), where index and heading.level are both
# monotonically increasing
open_headings = []
open_headings: list[tuple[int, Heading]] = []

# Add the lead section if appropriate:
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:
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.

The way a node is stripped is handled by the
@@ -674,7 +726,7 @@ class Wikicode(StringMixIn):
return stripped
return "".join(nodes)

def get_tree(self):
def get_tree(self) -> str:
"""Return a hierarchical tree representation of the object.

The representation is a string makes the most sense printed. It is


Завантаження…
Відмінити
Зберегти