Ver código fonte

Improve Wikicode/Node typing

tags/v0.7.2
Ben Kurtovic 1 ano atrás
pai
commit
36e6157197
14 arquivos alterados com 663 adições e 120 exclusões
  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 Ver arquivo

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

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

v0.7.1 (released June 30, 2025):



+ 1
- 0
docs/changelog.rst Ver arquivo

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

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

v0.7.1
------


+ 2
- 2
docs/integration.rst Ver arquivo

@@ -7,7 +7,7 @@ Integration
:func:`mwparserfromhell.parse() <mwparserfromhell.__init__.parse>` on
: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 pywikibot
@@ -19,7 +19,7 @@ If you're using Pywikibot_, your code might look like this:
return mwparserfromhell.parse(text)

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 requests


+ 0
- 9
mypy.ini Ver arquivo

@@ -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 Ver arquivo

@@ -72,3 +72,9 @@ extend-select = [
"I",
"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 Ver arquivo

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



+ 1
- 1
src/mwparserfromhell/nodes/heading.py Ver arquivo

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

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

def __str__(self) -> str:


+ 3
- 1
src/mwparserfromhell/nodes/template.py Ver arquivo

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

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

def add(


+ 1
- 1
src/mwparserfromhell/nodes/wikilink.py Ver arquivo

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

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

def __str__(self) -> str:


+ 0
- 0
Ver arquivo


+ 275
- 38
src/mwparserfromhell/string_mixin.py Ver arquivo

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

from __future__ import annotations

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

__all__ = ["StringMixIn"]

@@ -49,59 +51,294 @@ class StringMixIn:
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()

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 Ver arquivo

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

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

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


class Recurse(Enum):
RECURSE_OTHERS = 2


class Wikicode(StringMixIn):
"""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.
"""

RECURSE_OTHERS = 2
RECURSE_OTHERS = Recurse.RECURSE_OTHERS

def __init__(self, nodes: list[Node]):
super().__init__()
@@ -68,6 +73,24 @@ class Wikicode(StringMixIn):
def __str__(self) -> str:
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
def _get_children(
node: Node,
@@ -112,7 +135,7 @@ class Wikicode(StringMixIn):

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

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

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

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

def _do_weak_search(
self, obj: Any, recursive: bool
@@ -278,39 +302,6 @@ class Wikicode(StringMixIn):
node.__showtree__(write, get, mark)
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
def nodes(self) -> list[Node]:
"""A list of :class:`.Node` objects.
@@ -369,7 +360,9 @@ class Wikicode(StringMixIn):
return False
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.

Raises :exc:`ValueError` if *obj* is not found. If *recursive* is
@@ -496,7 +489,7 @@ class Wikicode(StringMixIn):
obj = str(obj)
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
) -> None:
"""Replace *obj* with *value*.
@@ -585,13 +578,32 @@ class Wikicode(StringMixIn):
return True
return False

@overload
def ifilter(
self,
recursive: bool = True,
recursive: bool | Literal[Recurse.RECURSE_OTHERS] = True,
matches: Callable[[Node], bool] | re.Pattern | str | None = None,
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.

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

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(
self,
@@ -768,16 +1081,3 @@ class Wikicode(StringMixIn):
"""
marker = object() # Random object we can find with certainty in a list
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 Ver arquivo

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

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


+ 13
- 8
tests/test_wikicode.py Ver arquivo

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

import pytest

@@ -123,11 +124,11 @@ def test_index():
code = parse("{{foo}}{{bar|{{baz}}}}")
assert 1 == code.index("{{bar|{{baz}}}}")
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):
code.index("{{baz}}", recursive=False)
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():
@@ -207,7 +208,7 @@ def _test_search(meth, expected):

code3 = parse("{{a|{{b}}|{{c|d={{f}}}}}}")
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):
func(obj, "x", recursive=False)
func(obj, "x", recursive=True)
@@ -238,7 +239,11 @@ def _test_search(meth, expected):
with pytest.raises(ValueError):
func("{{b}}{{c}}", "x", recursive=False)
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):
func(obj, "y", recursive=False)
func(obj, "y", recursive=True)
@@ -434,7 +439,7 @@ def test_filter_family():
assert list("abcdefg") == func(forcetype=Text)
assert [] == func(forcetype=Heading)
with pytest.raises(TypeError):
func(forcetype=True)
func(forcetype=True) # type: ignore

funcs = [
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", flags=0)
with pytest.raises(TypeError):
code.filter_templates(a=42)
code.filter_templates(a=42) # type: ignore
with pytest.raises(TypeError):
code.filter_templates(forcetype=Template)
code.filter_templates(forcetype=Template) # type: ignore
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>")
actual1 = code4.filter_templates(recursive=code4.RECURSE_OTHERS)


Carregando…
Cancelar
Salvar