Kaynağa Gözat

Improve Wikicode/Node typing

tags/v0.7.2
Ben Kurtovic 1 yıl önce
ebeveyn
işleme
36e6157197
14 değiştirilmiş dosya ile 663 ekleme ve 120 silme
  1. +1
    -0
      CHANGELOG
  2. +1
    -0
      docs/changelog.rst
  3. +2
    -2
      docs/integration.rst
  4. +0
    -9
      mypy.ini
  5. +6
    -0
      pyproject.toml
  6. +1
    -1
      src/mwparserfromhell/nodes/external_link.py
  7. +1
    -1
      src/mwparserfromhell/nodes/heading.py
  8. +3
    -1
      src/mwparserfromhell/nodes/template.py
  9. +1
    -1
      src/mwparserfromhell/nodes/wikilink.py
  10. +0
    -0
     
  11. +275
    -38
      src/mwparserfromhell/string_mixin.py
  12. +358
    -58
      src/mwparserfromhell/wikicode.py
  13. +1
    -1
      tests/test_string_mixin.py
  14. +13
    -8
      tests/test_wikicode.py

+ 1
- 0
CHANGELOG Dosyayı Görüntüle

@@ -1,6 +1,7 @@
v0.7.2 (unreleased): v0.7.2 (unreleased):


- Fix building on non-CPython implementations like PyPy. (#341) - Fix building on non-CPython implementations like PyPy. (#341)
- Improve typing.


v0.7.1 (released June 30, 2025): v0.7.1 (released June 30, 2025):




+ 1
- 0
docs/changelog.rst Dosyayı Görüntüle

@@ -9,6 +9,7 @@ Unreleased


- Fix building on non-CPython implementations like PyPy. - Fix building on non-CPython implementations like PyPy.
(`#341 <https://github.com/earwig/mwparserfromhell/issues/341>`_) (`#341 <https://github.com/earwig/mwparserfromhell/issues/341>`_)
- Improve typing.


v0.7.1 v0.7.1
------ ------


+ 2
- 2
docs/integration.rst Dosyayı Görüntüle

@@ -7,7 +7,7 @@ Integration
:func:`mwparserfromhell.parse() <mwparserfromhell.__init__.parse>` on :func:`mwparserfromhell.parse() <mwparserfromhell.__init__.parse>` on
:meth:`~earwigbot.wiki.page.Page.get`. :meth:`~earwigbot.wiki.page.Page.get`.


If you're using Pywikibot_, your code might look like this:
If you're using Pywikibot_, your code might look like this::


import mwparserfromhell import mwparserfromhell
import pywikibot import pywikibot
@@ -19,7 +19,7 @@ If you're using Pywikibot_, your code might look like this:
return mwparserfromhell.parse(text) return mwparserfromhell.parse(text)


If you're not using a library, you can parse any page with the following If you're not using a library, you can parse any page with the following
Python 3 code (using the API_ and the requests_ library):
Python 3 code (using the API_ and the requests_ library)::


import mwparserfromhell import mwparserfromhell
import requests import requests


+ 0
- 9
mypy.ini Dosyayı Görüntüle

@@ -1,9 +0,0 @@
# 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
- 0
pyproject.toml Dosyayı Görüntüle

@@ -72,3 +72,9 @@ extend-select = [
"I", "I",
"UP", "UP",
] ]

[tool.mypy]
disallow_incomplete_defs = true
warn_return_any = true
warn_unused_configs = true
warn_unused_ignores = true

+ 1
- 1
src/mwparserfromhell/nodes/external_link.py Dosyayı Görüntüle

@@ -44,7 +44,7 @@ class ExternalLink(Node):
): ):
super().__init__() super().__init__()
self.url = url self.url = url
self.title = title
self.title = title # pyright: ignore[reportIncompatibleMethodOverride]
self.brackets = brackets self.brackets = brackets
self.suppress_space = suppress_space self.suppress_space = suppress_space




+ 1
- 1
src/mwparserfromhell/nodes/heading.py Dosyayı Görüntüle

@@ -35,7 +35,7 @@ class Heading(Node):


def __init__(self, title: Any, level: int): def __init__(self, title: Any, level: int):
super().__init__() super().__init__()
self.title = title
self.title = title # pyright: ignore[reportIncompatibleMethodOverride]
self.level = level self.level = level


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


+ 3
- 1
src/mwparserfromhell/nodes/template.py Dosyayı Görüntüle

@@ -260,7 +260,9 @@ class Template(Node):
raise ValueError(name) raise ValueError(name)
return default return default


def __getitem__(self, name: str | Any) -> Parameter:
def __getitem__( # pyright: ignore[reportIncompatibleMethodOverride]
self, name: str | Any
) -> Parameter:
return self.get(name) return self.get(name)


def add( def add(


+ 1
- 1
src/mwparserfromhell/nodes/wikilink.py Dosyayı Görüntüle

@@ -37,7 +37,7 @@ class Wikilink(Node):


def __init__(self, title: Any, text: Any = None): def __init__(self, title: Any, text: Any = None):
super().__init__() super().__init__()
self.title = title
self.title = title # pyright: ignore[reportIncompatibleMethodOverride]
self.text = text self.text = text


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



+ 275
- 38
src/mwparserfromhell/string_mixin.py Dosyayı Görüntüle

@@ -25,7 +25,9 @@ interface for the ``str`` type in a dynamic manner.


from __future__ import annotations from __future__ import annotations


from sys import getdefaultencoding
import sys
from collections.abc import Iterable, Iterator, Mapping
from typing import Any, SupportsIndex


__all__ = ["StringMixIn"] __all__ = ["StringMixIn"]


@@ -49,59 +51,294 @@ class StringMixIn:
immutable ``self`` like the regular ``str`` type. immutable ``self`` like the regular ``str`` type.
""" """


def __str__(self):
# This is based on collections.UserString, but:
# - Requires overriding __str__ instead of setting .data
# - Returns new strings as strs instead of StringMixIns

__slots__ = ()

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


def __bytes__(self):
return bytes(self.__str__(), getdefaultencoding())
def __bytes__(self) -> bytes:
return bytes(str(self), sys.getdefaultencoding())

def __repr__(self) -> str:
return repr(str(self))

def __lt__(self, other: str | StringMixIn) -> bool:
return str(self) < other

def __le__(self, other: str | StringMixIn) -> bool:
return str(self) <= other

def __eq__(self, other: Any) -> bool:
return str(self) == other

def __ne__(self, other: Any) -> bool:
return str(self) != other

def __gt__(self, other: str | StringMixIn) -> bool:
return str(self) > other

def __ge__(self, other: str | StringMixIn) -> bool:
return str(self) >= other

def __bool__(self) -> bool:
return bool(str(self))

def __len__(self) -> int:
return len(str(self))

def __iter__(self) -> Iterator[str]:
yield from str(self)

def __getitem__(self, key: SupportsIndex | slice) -> str:
return str(self)[key]

def __reversed__(self) -> Iterator[str]:
return reversed(str(self))

def __contains__(self, item: Any) -> bool:
return str(item) in str(self)

@inheritdoc
def capitalize(self) -> str:
return str(self).capitalize()

@inheritdoc
def casefold(self) -> str:
return str(self).casefold()

@inheritdoc
def center(self, width: int, fillchar: str = " ") -> str:
return str(self).center(width, fillchar)

@inheritdoc
def count(
self,
sub: str | StringMixIn,
start: SupportsIndex | None = None,
end: SupportsIndex | None = None,
) -> int:
if isinstance(sub, StringMixIn):
sub = str(sub)
return str(self).count(sub, start, end)

@inheritdoc
def encode(self, encoding: str = "utf-8", errors: str = "strict") -> bytes:
return str(self).encode(encoding, errors)

@inheritdoc
def endswith(
self,
suffix: str | tuple[str, ...],
start: SupportsIndex | None = None,
end: SupportsIndex | None = None,
) -> bool:
return str(self).endswith(suffix, start, end)

@inheritdoc
def expandtabs(self, tabsize: int = 8) -> str:
return str(self).expandtabs(tabsize)

@inheritdoc
def find(
self,
sub: str | StringMixIn,
start: SupportsIndex | None = None,
end: SupportsIndex | None = None,
) -> int:
if isinstance(sub, StringMixIn):
sub = str(sub)
return str(self).find(sub, start, end)

@inheritdoc
def format(self, /, *args: Any, **kwds: Any) -> str:
return str(self).format(*args, **kwds)

@inheritdoc
def format_map(self, mapping: Mapping[str, Any]) -> str:
return str(self).format_map(mapping)

@inheritdoc
def index(
self,
sub: str,
start: SupportsIndex | None = None,
end: SupportsIndex | None = None,
) -> int:
return str(self).index(sub, start, end)

@inheritdoc
def isalpha(self) -> bool:
return str(self).isalpha()

@inheritdoc
def isalnum(self) -> bool:
return str(self).isalnum()

@inheritdoc
def isascii(self) -> bool:
return str(self).isascii()

@inheritdoc
def isdecimal(self) -> bool:
return str(self).isdecimal()

@inheritdoc
def isdigit(self) -> bool:
return str(self).isdigit()

@inheritdoc
def isidentifier(self) -> bool:
return str(self).isidentifier()

@inheritdoc
def islower(self) -> bool:
return str(self).islower()

@inheritdoc
def isnumeric(self) -> bool:
return str(self).isnumeric()

@inheritdoc
def isprintable(self) -> bool:
return str(self).isprintable()

@inheritdoc
def isspace(self) -> bool:
return str(self).isspace()

@inheritdoc
def istitle(self) -> bool:
return str(self).istitle()

@inheritdoc
def isupper(self) -> bool:
return str(self).isupper()

@inheritdoc
def join(self, seq: Iterable[str]) -> str:
return str(self).join(seq)

@inheritdoc
def ljust(self, width: int, fillchar: str = " ") -> str:
return str(self).ljust(width, fillchar)

@inheritdoc
def lower(self) -> str:
return str(self).lower()

@inheritdoc
def lstrip(self, chars: str | None = None) -> str:
return str(self).lstrip(chars)

maketrans = str.maketrans

@inheritdoc
def partition(self, sep: str) -> tuple[str, str, str]:
return str(self).partition(sep)

@inheritdoc
def removeprefix(self, prefix: str | StringMixIn, /) -> str:
if isinstance(prefix, StringMixIn):
prefix = str(prefix)
return str(self).removeprefix(prefix)


def __repr__(self):
return repr(self.__str__())
@inheritdoc
def removesuffix(self, suffix: str | StringMixIn, /) -> str:
if isinstance(suffix, StringMixIn):
suffix = str(suffix)
return str(self).removesuffix(suffix)


def __lt__(self, other):
return self.__str__() < other
@inheritdoc
def replace(
self,
old: str | StringMixIn,
new: str | StringMixIn,
/,
count: SupportsIndex = -1,
):
if isinstance(old, StringMixIn):
old = str(old)
if isinstance(new, StringMixIn):
new = str(new)
return str(self).replace(old, new, count)


def __le__(self, other):
return self.__str__() <= other
@inheritdoc
def rfind(
self,
sub: str | StringMixIn,
start: SupportsIndex | None = None,
end: SupportsIndex | None = None,
) -> int:
if isinstance(sub, StringMixIn):
sub = str(sub)
return str(self).rfind(sub, start, end)


def __eq__(self, other):
return self.__str__() == other
@inheritdoc
def rindex(
self,
sub: str,
start: SupportsIndex | None = None,
end: SupportsIndex | None = None,
) -> int:
return str(self).rindex(sub, start, end)


def __ne__(self, other):
return self.__str__() != other
@inheritdoc
def rjust(self, width: int, fillchar: str = " ") -> str:
return str(self).rjust(width, fillchar)


def __gt__(self, other):
return self.__str__() > other
@inheritdoc
def rpartition(self, sep: str) -> tuple[str, str, str]:
return str(self).rpartition(sep)


def __ge__(self, other):
return self.__str__() >= other
@inheritdoc
def rstrip(self, chars: str | None = None) -> str:
return str(self).rstrip(chars)


def __bool__(self):
return bool(self.__str__())
@inheritdoc
def split(self, sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str]:
return str(self).split(sep, maxsplit)


def __len__(self):
return len(self.__str__())
@inheritdoc
def rsplit(self, sep: str | None = None, maxsplit: SupportsIndex = -1) -> list[str]:
return str(self).rsplit(sep, maxsplit)


def __iter__(self):
yield from self.__str__()
@inheritdoc
def splitlines(self, keepends: bool = False) -> list[str]:
return str(self).splitlines(keepends)


def __getitem__(self, key):
return self.__str__()[key]
@inheritdoc
def startswith(
self,
prefix: str | tuple[str, ...],
start: SupportsIndex | None = None,
end: SupportsIndex | None = None,
) -> bool:
return str(self).startswith(prefix, start, end)


def __reversed__(self):
return reversed(self.__str__())
@inheritdoc
def strip(self, chars: str | None = None) -> str:
return str(self).strip(chars)


def __contains__(self, item):
return str(item) in self.__str__()
@inheritdoc
def swapcase(self) -> str:
return str(self).swapcase()


def __getattr__(self, attr):
if not hasattr(str, attr):
raise AttributeError(
f"{type(self).__name__!r} object has no attribute {attr!r}"
)
return getattr(self.__str__(), attr)
@inheritdoc
def title(self) -> str:
return str(self).title()


maketrans = str.maketrans # Static method can't rely on __getattr__
@inheritdoc
def translate(self, *args: Any) -> str:
return str(self).translate(*args)


@inheritdoc
def upper(self) -> str:
return str(self).upper()


del inheritdoc
@inheritdoc
def zfill(self, width: int) -> str:
return str(self).zfill(width)

+ 358
- 58
src/mwparserfromhell/wikicode.py Dosyayı Görüntüle

@@ -22,8 +22,9 @@ from __future__ import annotations


import re import re
from collections.abc import Generator, Iterable from collections.abc import Generator, Iterable
from enum import Enum
from itertools import chain from itertools import chain
from typing import Any, Callable, TypeVar, cast, overload
from typing import Any, Callable, Literal, TypeVar, cast, overload


from .nodes import ( from .nodes import (
Argument, Argument,
@@ -48,6 +49,10 @@ FLAGS = re.IGNORECASE | re.DOTALL
N = TypeVar("N", bound=Node) N = TypeVar("N", bound=Node)




class Recurse(Enum):
RECURSE_OTHERS = 2


class Wikicode(StringMixIn): class Wikicode(StringMixIn):
"""A ``Wikicode`` is a container for nodes that operates like a string. """A ``Wikicode`` is a container for nodes that operates like a string.


@@ -59,7 +64,7 @@ class Wikicode(StringMixIn):
over, for example, all of the templates in the object. over, for example, all of the templates in the object.
""" """


RECURSE_OTHERS = 2
RECURSE_OTHERS = Recurse.RECURSE_OTHERS


def __init__(self, nodes: list[Node]): def __init__(self, nodes: list[Node]):
super().__init__() super().__init__()
@@ -68,6 +73,24 @@ class Wikicode(StringMixIn):
def __str__(self) -> str: def __str__(self) -> str:
return "".join([str(node) for node in self.nodes]) return "".join([str(node) for node in self.nodes])


@overload
@staticmethod
def _get_children(
node: Node,
contexts: Literal[False] = False,
restrict: type | None = None,
parent: Wikicode | None = None,
) -> Generator[Node]: ...

@overload
@staticmethod
def _get_children(
node: Node,
contexts: Literal[True],
restrict: type | None = None,
parent: Wikicode | None = None,
) -> Generator[tuple[Wikicode | None, Node]]: ...

@staticmethod @staticmethod
def _get_children( def _get_children(
node: Node, node: Node,
@@ -112,7 +135,7 @@ class Wikicode(StringMixIn):


def _indexed_ifilter( def _indexed_ifilter(
self, self,
recursive: bool = True,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[N], bool] | re.Pattern | str | None = None, matches: Callable[[N], bool] | re.Pattern | str | None = None,
flags: int = FLAGS, flags: int = FLAGS,
forcetype: type[N] | None = None, forcetype: type[N] | None = None,
@@ -182,7 +205,7 @@ class Wikicode(StringMixIn):
raise ValueError(obj) raise ValueError(obj)
return obj, slice(0, len(obj.nodes)) return obj, slice(0, len(obj.nodes))


if isinstance(obj, Node):
elif isinstance(obj, Node):


def mkslice(i): def mkslice(i):
return slice(i, i + 1) return slice(i, i + 1)
@@ -197,7 +220,8 @@ class Wikicode(StringMixIn):
return context, mkslice(context.index(child)) return context, mkslice(context.index(child))
raise ValueError(obj) raise ValueError(obj)


raise TypeError(obj)
else:
raise TypeError(obj)


def _do_weak_search( def _do_weak_search(
self, obj: Any, recursive: bool self, obj: Any, recursive: bool
@@ -278,39 +302,6 @@ class Wikicode(StringMixIn):
node.__showtree__(write, get, mark) node.__showtree__(write, get, mark)
return lines return lines


@classmethod
def _build_filter_methods(cls, **meths):
"""Given Node types, build the corresponding i?filter shortcuts.

The should be given as keys storing the method's base name paired with
values storing the corresponding :class:`.Node` type. For example, the
dict may contain the pair ``("templates", Template)``, which will
produce the methods :meth:`ifilter_templates` and
:meth:`filter_templates`, which are shortcuts for
:meth:`ifilter(forcetype=Template) <ifilter>` and
:meth:`filter(forcetype=Template) <filter>`, respectively. These
shortcuts are added to the class itself, with an appropriate docstring.
"""
doc = """Iterate over {0}.

This is equivalent to :meth:`{1}` with *forcetype* set to
:class:`~{2.__module__}.{2.__name__}`.
"""

def make_ifilter(ftype):
return lambda self, *a, **kw: self.ifilter(forcetype=ftype, *a, **kw)

def make_filter(ftype):
return lambda self, *a, **kw: self.filter(forcetype=ftype, *a, **kw)

for name, ftype in meths.items():
ifilt = make_ifilter(ftype)
filt = make_filter(ftype)
ifilt.__doc__ = doc.format(name, "ifilter", ftype)
filt.__doc__ = doc.format(name, "filter", ftype)
setattr(cls, "ifilter_" + name, ifilt)
setattr(cls, "filter_" + name, filt)

@property @property
def nodes(self) -> list[Node]: def nodes(self) -> list[Node]:
"""A list of :class:`.Node` objects. """A list of :class:`.Node` objects.
@@ -369,7 +360,9 @@ class Wikicode(StringMixIn):
return False return False
return True return True


def index(self, obj: Node | Wikicode | str, recursive: bool = False) -> int:
def index( # pyright: ignore[reportIncompatibleMethodOverride]
self, obj: Node | Wikicode | str, recursive: bool = False
) -> int:
"""Return the index of *obj* in the list of nodes. """Return the index of *obj* in the list of nodes.


Raises :exc:`ValueError` if *obj* is not found. If *recursive* is Raises :exc:`ValueError` if *obj* is not found. If *recursive* is
@@ -496,7 +489,7 @@ class Wikicode(StringMixIn):
obj = str(obj) obj = str(obj)
self._slice_replace(context, index, obj, obj + str(value)) self._slice_replace(context, index, obj, obj + str(value))


def replace(
def replace( # pyright: ignore[reportIncompatibleMethodOverride]
self, obj: Node | Wikicode | str, value: Any, recursive: bool = True self, obj: Node | Wikicode | str, value: Any, recursive: bool = True
) -> None: ) -> None:
"""Replace *obj* with *value*. """Replace *obj* with *value*.
@@ -585,13 +578,32 @@ class Wikicode(StringMixIn):
return True return True
return False return False


@overload
def ifilter( def ifilter(
self, self,
recursive: bool = True,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Node], bool] | re.Pattern | str | None = None, matches: Callable[[Node], bool] | re.Pattern | str | None = None,
flags: int = FLAGS, flags: int = FLAGS,
forcetype: type | None = None,
) -> Generator[Node]:
forcetype: None = None,
) -> Generator[Node]: ...

@overload
def ifilter(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[N], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
*,
forcetype: type[N],
) -> Generator[N]: ...

def ifilter(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[N], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
forcetype: type[N] | None = None,
) -> Generator[N]:
"""Iterate over nodes in our list matching certain conditions. """Iterate over nodes in our list matching certain conditions.


If *forcetype* is given, only nodes that are instances of this type (or If *forcetype* is given, only nodes that are instances of this type (or
@@ -618,12 +630,313 @@ class Wikicode(StringMixIn):
gen = self._indexed_ifilter(recursive, matches, flags, forcetype) gen = self._indexed_ifilter(recursive, matches, flags, forcetype)
return (node for i, node in gen) return (node for i, node in gen)


def filter(self, *args: Any, **kwargs: Any) -> list[Node]:
def ifilter_arguments(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Argument], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> Generator[Argument]:
"""Iterate over arguments.

This is equivalent to :meth:`ifilter` with *forcetype* set to
:class:`~argument.Argument`.
"""
return self.ifilter(
recursive=recursive, matches=matches, flags=flags, forcetype=Argument
)

def ifilter_comments(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Comment], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> Generator[Comment]:
"""Iterate over comments.

This is equivalent to :meth:`ifilter` with *forcetype* set to
:class:`~comment.Comment`.
"""
return self.ifilter(
recursive=recursive, matches=matches, flags=flags, forcetype=Comment
)

def ifilter_external_links(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[ExternalLink], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> Generator[ExternalLink]:
"""Iterate over external links.

This is equivalent to :meth:`ifilter` with *forcetype* set to
:class:`~external_link.ExternalLink`.
"""
return self.ifilter(
recursive=recursive, matches=matches, flags=flags, forcetype=ExternalLink
)

def ifilter_headings(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Heading], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> Generator[Heading]:
"""Iterate over headings.

This is equivalent to :meth:`ifilter` with *forcetype* set to
:class:`~heading.Heading`.
"""
return self.ifilter(
recursive=recursive, matches=matches, flags=flags, forcetype=Heading
)

def ifilter_html_entities(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[HTMLEntity], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> Generator[HTMLEntity]:
"""Iterate over HTML entities.

This is equivalent to :meth:`ifilter` with *forcetype* set to
:class:`~html_entity.HTMLEntity`.
"""
return self.ifilter(
recursive=recursive, matches=matches, flags=flags, forcetype=HTMLEntity
)

def ifilter_tags(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Tag], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> Generator[Tag]:
"""Iterate over tags.

This is equivalent to :meth:`ifilter` with *forcetype* set to
:class:`~tag.Tag`.
"""
return self.ifilter(
recursive=recursive, matches=matches, flags=flags, forcetype=Tag
)

def ifilter_templates(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Template], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> Generator[Template]:
"""Iterate over templates.

This is equivalent to :meth:`ifilter` with *forcetype* set to
:class:`~template.Template`.
"""
return self.ifilter(
recursive=recursive, matches=matches, flags=flags, forcetype=Template
)

def ifilter_text(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Text], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> Generator[Text]:
"""Iterate over text.

This is equivalent to :meth:`ifilter` with *forcetype* set to
:class:`~text.Text`.
"""
return self.ifilter(
recursive=recursive, matches=matches, flags=flags, forcetype=Text
)

def ifilter_wikilinks(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Wikilink], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> Generator[Wikilink]:
"""Iterate over wikilinks.

This is equivalent to :meth:`ifilter` with *forcetype* set to
:class:`~wikilink.Wikilink`.
"""
return self.ifilter(
recursive=recursive, matches=matches, flags=flags, forcetype=Wikilink
)

@overload
def filter(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Node], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
forcetype: None = None,
) -> list[Node]: ...

@overload
def filter(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[N], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
*,
forcetype: type[N],
) -> list[N]: ...

def filter(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[N], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
forcetype: type[N] | None = None,
) -> list[N]:
"""Return a list of nodes within our list matching certain conditions. """Return a list of nodes within our list matching certain conditions.


This is equivalent to calling :func:`list` on :meth:`ifilter`. This is equivalent to calling :func:`list` on :meth:`ifilter`.
""" """
return list(self.ifilter(*args, **kwargs))
gen = self.ifilter( # pyright: ignore[reportCallIssue]
recursive=recursive,
matches=matches,
flags=flags,
forcetype=forcetype, # pyright: ignore[reportArgumentType]
)
return list(gen)

def filter_arguments(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Argument], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> list[Argument]:
"""Iterate over arguments.

This is equivalent to :meth:`filter` with *forcetype* set to
:class:`~argument.Argument`.
"""
return self.filter(
recursive=recursive, matches=matches, flags=flags, forcetype=Argument
)

def filter_comments(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Comment], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> list[Comment]:
"""Iterate over comments.

This is equivalent to :meth:`filter` with *forcetype* set to
:class:`~comment.Comment`.
"""
return self.filter(
recursive=recursive, matches=matches, flags=flags, forcetype=Comment
)

def filter_external_links(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[ExternalLink], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> list[ExternalLink]:
"""Iterate over external links.

This is equivalent to :meth:`filter` with *forcetype* set to
:class:`~external_link.ExternalLink`.
"""
return self.filter(
recursive=recursive, matches=matches, flags=flags, forcetype=ExternalLink
)

def filter_headings(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Heading], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> list[Heading]:
"""Iterate over headings.

This is equivalent to :meth:`filter` with *forcetype* set to
:class:`~heading.Heading`.
"""
return self.filter(
recursive=recursive, matches=matches, flags=flags, forcetype=Heading
)

def filter_html_entities(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[HTMLEntity], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> list[HTMLEntity]:
"""Iterate over HTML entities.

This is equivalent to :meth:`filter` with *forcetype* set to
:class:`~html_entity.HTMLEntity`.
"""
return self.filter(
recursive=recursive, matches=matches, flags=flags, forcetype=HTMLEntity
)

def filter_tags(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Tag], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> list[Tag]:
"""Iterate over tags.

This is equivalent to :meth:`filter` with *forcetype* set to
:class:`~tag.Tag`.
"""
return self.filter(
recursive=recursive, matches=matches, flags=flags, forcetype=Tag
)

def filter_templates(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Template], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> list[Template]:
"""Iterate over templates.

This is equivalent to :meth:`filter` with *forcetype* set to
:class:`~template.Template`.
"""
return self.filter(
recursive=recursive, matches=matches, flags=flags, forcetype=Template
)

def filter_text(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Text], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> list[Text]:
"""Iterate over text.

This is equivalent to :meth:`filter` with *forcetype* set to
:class:`~text.Text`.
"""
return self.filter(
recursive=recursive, matches=matches, flags=flags, forcetype=Text
)

def filter_wikilinks(
self,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Wikilink], bool] | re.Pattern | str | None = None,
flags: int = FLAGS,
) -> list[Wikilink]:
"""Iterate over wikilinks.

This is equivalent to :meth:`filter` with *forcetype* set to
:class:`~wikilink.Wikilink`.
"""
return self.filter(
recursive=recursive, matches=matches, flags=flags, forcetype=Wikilink
)


def get_sections( def get_sections(
self, self,
@@ -768,16 +1081,3 @@ class Wikicode(StringMixIn):
""" """
marker = object() # Random object we can find with certainty in a list marker = object() # Random object we can find with certainty in a list
return "\n".join(self._get_tree(self, [], marker, 0)) return "\n".join(self._get_tree(self, [], marker, 0))


Wikicode._build_filter_methods(
arguments=Argument,
comments=Comment,
external_links=ExternalLink,
headings=Heading,
html_entities=HTMLEntity,
tags=Tag,
templates=Template,
text=Text,
wikilinks=Wikilink,
)

+ 1
- 1
tests/test_string_mixin.py Dosyayı Görüntüle

@@ -271,7 +271,7 @@ def test_other_methods():
assert "fake string" == str1.format_map({}) assert "fake string" == str1.format_map({})
assert "foobarbaz" == str6.format_map({"abc": "bar"}) assert "foobarbaz" == str6.format_map({"abc": "bar"})
with pytest.raises(ValueError): with pytest.raises(ValueError):
str5.format_map({0: "abc"})
str5.format_map({0: "abc"}) # type: ignore


assert 3 == str1.index("e") assert 3 == str1.index("e")
with pytest.raises(ValueError): with pytest.raises(ValueError):


+ 13
- 8
tests/test_wikicode.py Dosyayı Görüntüle

@@ -28,6 +28,7 @@ import pickle
import re import re
from functools import partial from functools import partial
from types import GeneratorType from types import GeneratorType
from typing import cast


import pytest import pytest


@@ -123,11 +124,11 @@ def test_index():
code = parse("{{foo}}{{bar|{{baz}}}}") code = parse("{{foo}}{{bar|{{baz}}}}")
assert 1 == code.index("{{bar|{{baz}}}}") assert 1 == code.index("{{bar|{{baz}}}}")
assert 1 == code.index("{{baz}}", recursive=True) assert 1 == code.index("{{baz}}", recursive=True)
assert 1 == code.index(code.get(1).get(1).value, recursive=True)
assert 1 == code.index(cast(Template, code.get(1)).get(1).value, recursive=True)
with pytest.raises(ValueError): with pytest.raises(ValueError):
code.index("{{baz}}", recursive=False) code.index("{{baz}}", recursive=False)
with pytest.raises(ValueError): with pytest.raises(ValueError):
code.index(code.get(1).get(1).value, recursive=False)
code.index(cast(Template, code.get(1)).get(1).value, recursive=False)




def test_get_ancestors_parent(): def test_get_ancestors_parent():
@@ -207,7 +208,7 @@ def _test_search(meth, expected):


code3 = parse("{{a|{{b}}|{{c|d={{f}}}}}}") code3 = parse("{{a|{{b}}|{{c|d={{f}}}}}}")
func = partial(meth, code3) func = partial(meth, code3)
obj = code3.get(0).params[0].value.get(0)
obj = cast(Template, code3.get(0)).params[0].value.get(0)
with pytest.raises(ValueError): with pytest.raises(ValueError):
func(obj, "x", recursive=False) func(obj, "x", recursive=False)
func(obj, "x", recursive=True) func(obj, "x", recursive=True)
@@ -238,7 +239,11 @@ def _test_search(meth, expected):
with pytest.raises(ValueError): with pytest.raises(ValueError):
func("{{b}}{{c}}", "x", recursive=False) func("{{b}}{{c}}", "x", recursive=False)
func("{{b}}{{c}}", "x", recursive=True) func("{{b}}{{c}}", "x", recursive=True)
obj = code5.get(0).params[1].value.get(0).params[0].value
obj = (
cast(Template, cast(Template, code5.get(0)).params[1].value.get(0))
.params[0]
.value
)
with pytest.raises(ValueError): with pytest.raises(ValueError):
func(obj, "y", recursive=False) func(obj, "y", recursive=False)
func(obj, "y", recursive=True) func(obj, "y", recursive=True)
@@ -434,7 +439,7 @@ def test_filter_family():
assert list("abcdefg") == func(forcetype=Text) assert list("abcdefg") == func(forcetype=Text)
assert [] == func(forcetype=Heading) assert [] == func(forcetype=Heading)
with pytest.raises(TypeError): with pytest.raises(TypeError):
func(forcetype=True)
func(forcetype=True) # type: ignore


funcs = [ funcs = [
lambda name, **kw: getattr(code, "filter_" + name)(**kw), lambda name, **kw: getattr(code, "filter_" + name)(**kw),
@@ -497,11 +502,11 @@ def test_filter_family():
assert [] == code3.filter_tags(matches=r"^{{b.*?z") assert [] == code3.filter_tags(matches=r"^{{b.*?z")
assert [] == code3.filter_tags(matches=r"^{{b.*?z", flags=0) assert [] == code3.filter_tags(matches=r"^{{b.*?z", flags=0)
with pytest.raises(TypeError): with pytest.raises(TypeError):
code.filter_templates(a=42)
code.filter_templates(a=42) # type: ignore
with pytest.raises(TypeError): with pytest.raises(TypeError):
code.filter_templates(forcetype=Template)
code.filter_templates(forcetype=Template) # type: ignore
with pytest.raises(TypeError): with pytest.raises(TypeError):
code.filter_templates(1, 0, 0, Template)
code.filter_templates(1, 0, 0, Template) # type: ignore


code4 = parse("{{foo}}<b>{{foo|{{bar}}}}</b>") code4 = parse("{{foo}}<b>{{foo|{{bar}}}}</b>")
actual1 = code4.filter_templates(recursive=code4.RECURSE_OTHERS) actual1 = code4.filter_templates(recursive=code4.RECURSE_OTHERS)


Yükleniyor…
İptal
Kaydet