diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..622c3c6 --- /dev/null +++ b/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 diff --git a/src/mwparserfromhell/definitions.py b/src/mwparserfromhell/definitions.py index 3258063..14c4ade 100644 --- a/src/mwparserfromhell/definitions.py +++ b/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: diff --git a/src/mwparserfromhell/nodes/_base.py b/src/mwparserfromhell/nodes/_base.py index 8ea36d3..f34e78d 100644 --- a/src/mwparserfromhell/nodes/_base.py +++ b/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)) diff --git a/src/mwparserfromhell/nodes/argument.py b/src/mwparserfromhell/nodes/argument.py index f590245..48e9514 100644 --- a/src/mwparserfromhell/nodes/argument.py +++ b/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: diff --git a/src/mwparserfromhell/nodes/comment.py b/src/mwparserfromhell/nodes/comment.py index f2a6a6d..2631efc 100644 --- a/src/mwparserfromhell/nodes/comment.py +++ b/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 ````.""" - def __init__(self, contents): + def __init__(self, contents: str): super().__init__() self.contents = contents - def __str__(self): + def __str__(self) -> str: return "" @property - def contents(self): + def contents(self) -> str: """The hidden text contained between ````.""" return self._contents @contents.setter - def contents(self, value): + def contents(self, value: Any) -> None: self._contents = str(value) diff --git a/src/mwparserfromhell/nodes/external_link.py b/src/mwparserfromhell/nodes/external_link.py index 85f1fae..70a23dd 100644 --- a/src/mwparserfromhell/nodes/external_link.py +++ b/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) diff --git a/src/mwparserfromhell/nodes/extras/attribute.py b/src/mwparserfromhell/nodes/extras/attribute.py index fc3421a..98f8c13 100644 --- a/src/mwparserfromhell/nodes/extras/attribute.py +++ b/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) diff --git a/src/mwparserfromhell/nodes/extras/parameter.py b/src/mwparserfromhell/nodes/extras/parameter.py index 6352f5f..dac3ecc 100644 --- a/src/mwparserfromhell/nodes/extras/parameter.py +++ b/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)) diff --git a/src/mwparserfromhell/nodes/heading.py b/src/mwparserfromhell/nodes/heading.py index b65cf29..9c1b122 100644 --- a/src/mwparserfromhell/nodes/heading.py +++ b/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) diff --git a/src/mwparserfromhell/nodes/html_entity.py b/src/mwparserfromhell/nodes/html_entity.py index 2322e38..2e77120 100644 --- a/src/mwparserfromhell/nodes/html_entity.py +++ b/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 `` ``, 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, ``Σ``, ``Σ``, and ``Σ`` 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 ``"ሴ"`` is ``"x"``, whereas the - hex_char of ``"ሴ"`` 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, ``Σ``, ``Σ``, and ``Σ`` 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 ``"ሴ"`` is ``"x"``, whereas the + hex_char of ``"ሴ"`` 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]) diff --git a/src/mwparserfromhell/nodes/tag.py b/src/mwparserfromhell/nodes/tag.py index 027b09c..bf4ad40 100644 --- a/src/mwparserfromhell/nodes/tag.py +++ b/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 += "" 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("") @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 ``
``).""" 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 ``
`` 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. diff --git a/src/mwparserfromhell/nodes/template.py b/src/mwparserfromhell/nodes/template.py index e3abe79..50f4193 100644 --- a/src/mwparserfromhell/nodes/template.py +++ b/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) diff --git a/src/mwparserfromhell/nodes/text.py b/src/mwparserfromhell/nodes/text.py index 6fea574..6847645 100644 --- a/src/mwparserfromhell/nodes/text.py +++ b/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) diff --git a/src/mwparserfromhell/nodes/wikilink.py b/src/mwparserfromhell/nodes/wikilink.py index 870cba9..3378778 100644 --- a/src/mwparserfromhell/nodes/wikilink.py +++ b/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: diff --git a/src/mwparserfromhell/wikicode.py b/src/mwparserfromhell/wikicode.py index 4d4f9b3..a9cdce9 100644 --- a/src/mwparserfromhell/wikicode.py +++ b/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