| @@ -227,7 +227,7 @@ class Tag(Node): | |||||
| return self._padding | return self._padding | ||||
| @padding.setter | @padding.setter | ||||
| def padding(self, value: str) -> None: | |||||
| def padding(self, value: str | None) -> None: | |||||
| if not value: | if not value: | ||||
| self._padding = "" | self._padding = "" | ||||
| else: | else: | ||||
| @@ -278,7 +278,7 @@ class Tag(Node): | |||||
| def closing_wiki_markup(self, value: str | None) -> None: | def closing_wiki_markup(self, value: str | None) -> None: | ||||
| self._closing_wiki_markup = str(value) if value else None | self._closing_wiki_markup = str(value) if value else None | ||||
| def has(self, name: str) -> bool: | |||||
| def has(self, name: str | Attribute | Wikicode) -> bool: | |||||
| """Return whether any attribute in the tag has the given *name*. | """Return whether any attribute in the tag has the given *name*. | ||||
| Note that a tag may have multiple attributes with the same name, but | Note that a tag may have multiple attributes with the same name, but | ||||
| @@ -289,7 +289,7 @@ class Tag(Node): | |||||
| return True | return True | ||||
| return False | return False | ||||
| def get(self, name: str) -> Attribute: | |||||
| def get(self, name: str | Attribute | Wikicode) -> Attribute: | |||||
| """Get the attribute with the given *name*. | """Get the attribute with the given *name*. | ||||
| The returned object is a :class:`.Attribute` instance. Raises | The returned object is a :class:`.Attribute` instance. Raises | ||||
| @@ -42,7 +42,6 @@ if TYPE_CHECKING: | |||||
| __all__ = ["Template"] | __all__ = ["Template"] | ||||
| FLAGS = re.DOTALL | re.UNICODE | |||||
| # Used to allow None as a valid fallback value | # Used to allow None as a valid fallback value | ||||
| _UNSET = object() | _UNSET = object() | ||||
| @@ -134,7 +133,7 @@ class Template(Node): | |||||
| if sval.isspace(): | if sval.isspace(): | ||||
| before, after = "", sval | before, after = "", sval | ||||
| else: | else: | ||||
| match = re.search(r"^(\s*).*?(\s*)$", sval, FLAGS) | |||||
| match = re.search(r"^(\s*).*?(\s*)$", sval, re.DOTALL) | |||||
| assert match, sval | assert match, sval | ||||
| before, after = match.group(1), match.group(2) | before, after = match.group(1), match.group(2) | ||||
| value.nodes = [Text(before), Text(after)] | value.nodes = [Text(before), Text(after)] | ||||
| @@ -157,7 +156,7 @@ class Template(Node): | |||||
| component = str(param.name) | component = str(param.name) | ||||
| else: | else: | ||||
| component = str(param.value) | component = str(param.value) | ||||
| match = re.search(r"^(\s*).*?(\s*)$", component, FLAGS) | |||||
| match = re.search(r"^(\s*).*?(\s*)$", component, re.DOTALL) | |||||
| assert match, component | assert match, component | ||||
| before, after = match.group(1), match.group(2) | before, after = match.group(1), match.group(2) | ||||
| if not use_names and component.isspace() and "\n" in before: | if not use_names and component.isspace() and "\n" in before: | ||||
| @@ -377,7 +376,7 @@ class Template(Node): | |||||
| def __setitem__(self, name: Any, value: Any) -> Parameter: | def __setitem__(self, name: Any, value: Any) -> Parameter: | ||||
| return self.add(name, value) | return self.add(name, value) | ||||
| def remove(self, param: Parameter | str, keep_field: bool = False) -> None: | |||||
| def remove(self, param: Parameter | str | int, keep_field: bool = False) -> None: | |||||
| """Remove a parameter from the template, identified by *param*. | """Remove a parameter from the template, identified by *param*. | ||||
| If *param* is a :class:`.Parameter` object, it will be matched exactly, | If *param* is a :class:`.Parameter` object, it will be matched exactly, | ||||
| @@ -43,7 +43,7 @@ from .utils import parse_anything | |||||
| __all__ = ["Wikicode"] | __all__ = ["Wikicode"] | ||||
| FLAGS = re.IGNORECASE | re.DOTALL | re.UNICODE | |||||
| FLAGS = re.IGNORECASE | re.DOTALL | |||||
| N = TypeVar("N", bound=Node) | N = TypeVar("N", bound=Node) | ||||
| @@ -53,7 +53,7 @@ def test_showtree(): | |||||
| """test Comment.__showtree__()""" | """test Comment.__showtree__()""" | ||||
| output = [] | output = [] | ||||
| node = Comment("foobar") | node = Comment("foobar") | ||||
| node.__showtree__(output.append, None, None) | |||||
| node.__showtree__(output.append, lambda _code: None, lambda: None) | |||||
| assert ["<!--foobar-->"] == output | assert ["<!--foobar-->"] == output | ||||
| @@ -64,8 +64,8 @@ def test_showtree(): | |||||
| node1 = Heading(wraptext("foobar"), 3) | node1 = Heading(wraptext("foobar"), 3) | ||||
| node2 = Heading(wraptext(" baz "), 4) | node2 = Heading(wraptext(" baz "), 4) | ||||
| node1.__showtree__(output.append, get, None) | |||||
| node2.__showtree__(output.append, get, None) | |||||
| node1.__showtree__(output.append, get, lambda: None) | |||||
| node2.__showtree__(output.append, get, lambda: None) | |||||
| valid = ["===", (getter, node1.title), "===", "====", (getter, node2.title), "===="] | valid = ["===", (getter, node1.title), "===", "====", (getter, node2.title), "===="] | ||||
| assert valid == output | assert valid == output | ||||
| @@ -69,9 +69,9 @@ def test_showtree(): | |||||
| node1 = HTMLEntity("nbsp", named=True, hexadecimal=False) | node1 = HTMLEntity("nbsp", named=True, hexadecimal=False) | ||||
| node2 = HTMLEntity("107", named=False, hexadecimal=False) | node2 = HTMLEntity("107", named=False, hexadecimal=False) | ||||
| node3 = HTMLEntity("e9", named=False, hexadecimal=True) | node3 = HTMLEntity("e9", named=False, hexadecimal=True) | ||||
| node1.__showtree__(output.append, None, None) | |||||
| node2.__showtree__(output.append, None, None) | |||||
| node3.__showtree__(output.append, None, None) | |||||
| node1.__showtree__(output.append, lambda _code: None, lambda: None) | |||||
| node2.__showtree__(output.append, lambda _code: None, lambda: None) | |||||
| node3.__showtree__(output.append, lambda _code: None, lambda: None) | |||||
| res = [" ", "k", "é"] | res = [" ", "k", "é"] | ||||
| assert res == output | assert res == output | ||||
| @@ -125,9 +125,9 @@ def test_named(): | |||||
| assert node1.named is True | assert node1.named is True | ||||
| assert node2.named is False | assert node2.named is False | ||||
| assert node3.named is False | assert node3.named is False | ||||
| node1.named = 1 | |||||
| node2.named = 0 | |||||
| node3.named = 0 | |||||
| node1.named = True | |||||
| node2.named = False | |||||
| node3.named = False | |||||
| assert node1.named is True | assert node1.named is True | ||||
| assert node2.named is False | assert node2.named is False | ||||
| assert node3.named is False | assert node3.named is False | ||||
| @@ -208,7 +208,7 @@ def test_wiki_markup(): | |||||
| node.wiki_markup = "''" | node.wiki_markup = "''" | ||||
| assert "''" == node.wiki_markup | assert "''" == node.wiki_markup | ||||
| assert "''italic text''" == node | assert "''italic text''" == node | ||||
| node.wiki_markup = False | |||||
| node.wiki_markup = None | |||||
| assert node.wiki_markup is None | assert node.wiki_markup is None | ||||
| assert "<i>italic text</i>" == node | assert "<i>italic text</i>" == node | ||||
| @@ -220,7 +220,7 @@ def test_self_closing(): | |||||
| node.self_closing = True | node.self_closing = True | ||||
| assert node.self_closing is True | assert node.self_closing is True | ||||
| assert "<ref/>" == node | assert "<ref/>" == node | ||||
| node.self_closing = 0 | |||||
| node.self_closing = False | |||||
| assert node.self_closing is False | assert node.self_closing is False | ||||
| assert "<ref>foobar</ref>" == node | assert "<ref>foobar</ref>" == node | ||||
| @@ -232,7 +232,7 @@ def test_invalid(): | |||||
| node.invalid = True | node.invalid = True | ||||
| assert node.invalid is True | assert node.invalid is True | ||||
| assert "</br>" == node | assert "</br>" == node | ||||
| node.invalid = 0 | |||||
| node.invalid = False | |||||
| assert node.invalid is False | assert node.invalid is False | ||||
| assert "<br>" == node | assert "<br>" == node | ||||
| @@ -244,7 +244,7 @@ def test_implicit(): | |||||
| node.implicit = True | node.implicit = True | ||||
| assert node.implicit is True | assert node.implicit is True | ||||
| assert "<br>" == node | assert "<br>" == node | ||||
| node.implicit = 0 | |||||
| node.implicit = False | |||||
| assert node.implicit is False | assert node.implicit is False | ||||
| assert "<br/>" == node | assert "<br/>" == node | ||||
| @@ -298,7 +298,7 @@ def test_closing_wiki_markup(): | |||||
| node.wiki_markup = "!!" | node.wiki_markup = "!!" | ||||
| assert "|}" == node.closing_wiki_markup | assert "|}" == node.closing_wiki_markup | ||||
| assert "!!\n|}" == node | assert "!!\n|}" == node | ||||
| node.wiki_markup = False | |||||
| node.wiki_markup = None | |||||
| assert node.closing_wiki_markup is None | assert node.closing_wiki_markup is None | ||||
| assert "<table>\n</table>" == node | assert "<table>\n</table>" == node | ||||
| node2 = Tag( | node2 = Tag( | ||||
| @@ -57,9 +57,9 @@ def test_showtree(): | |||||
| node1 = Text("foobar") | node1 = Text("foobar") | ||||
| node2 = Text("fóóbar") | node2 = Text("fóóbar") | ||||
| node3 = Text("𐌲𐌿𐍄") | node3 = Text("𐌲𐌿𐍄") | ||||
| node1.__showtree__(output.append, None, None) | |||||
| node2.__showtree__(output.append, None, None) | |||||
| node3.__showtree__(output.append, None, None) | |||||
| node1.__showtree__(output.append, lambda _code: None, lambda: None) | |||||
| node2.__showtree__(output.append, lambda _code: None, lambda: None) | |||||
| node3.__showtree__(output.append, lambda _code: None, lambda: None) | |||||
| res = ["foobar", r"f\xf3\xf3bar", "\\U00010332\\U0001033f\\U00010344"] | res = ["foobar", r"f\xf3\xf3bar", "\\U00010332\\U0001033f\\U00010344"] | ||||
| assert res == output | assert res == output | ||||
| @@ -20,9 +20,9 @@ | |||||
| from __future__ import annotations | from __future__ import annotations | ||||
| import codecs | |||||
| import warnings | |||||
| from os import listdir, path | |||||
| import os | |||||
| from collections.abc import Generator | |||||
| from dataclasses import dataclass | |||||
| import pytest | import pytest | ||||
| @@ -40,71 +40,79 @@ class _TestParseError(Exception): | |||||
| """Raised internally when a test could not be parsed.""" | """Raised internally when a test could not be parsed.""" | ||||
| def _parse_test(test, data): | |||||
| """Parse an individual *test*, storing its info in *data*.""" | |||||
| for line in test.strip().splitlines(): | |||||
| if line.startswith("name:"): | |||||
| data["name"] = line[len("name:") :].strip() | |||||
| elif line.startswith("label:"): | |||||
| data["label"] = line[len("label:") :].strip() | |||||
| elif line.startswith("input:"): | |||||
| raw = line[len("input:") :].strip() | |||||
| if raw[0] == '"' and raw[-1] == '"': | |||||
| raw = raw[1:-1] | |||||
| raw = raw.encode("raw_unicode_escape") | |||||
| data["input"] = raw.decode("unicode_escape") | |||||
| elif line.startswith("output:"): | |||||
| raw = line[len("output:") :].strip() | |||||
| try: | |||||
| data["output"] = eval(raw, vars(tokens)) | |||||
| except Exception as err: | |||||
| raise _TestParseError(err) from err | |||||
| def _load_tests(filename, name, text): | |||||
| """Load all tests in *text* from the file *filename*.""" | |||||
| @dataclass | |||||
| class _TestCase: | |||||
| name: str | |||||
| label: str | None | |||||
| input: str | |||||
| output: list[tokens.Token] | |||||
| def _load_tests(file_path, filename, text) -> Generator[_TestCase]: | |||||
| tests = text.split("\n---\n") | tests = text.split("\n---\n") | ||||
| for test in tests: | for test in tests: | ||||
| data = {"name": None, "label": None, "input": None, "output": None} | |||||
| try: | |||||
| _parse_test(test, data) | |||||
| except _TestParseError as err: | |||||
| if data["name"]: | |||||
| error = "Could not parse test '{0}' in '{1}':\n\t{2}" | |||||
| warnings.warn(error.format(data["name"], filename, err)) | |||||
| else: | |||||
| error = "Could not parse a test in '{0}':\n\t{1}" | |||||
| warnings.warn(error.format(filename, err)) | |||||
| continue | |||||
| if not data["name"]: | |||||
| error = "A test in '{0}' was ignored because it lacked a name" | |||||
| warnings.warn(error.format(filename)) | |||||
| name = None | |||||
| label = None | |||||
| input = None | |||||
| output = None | |||||
| for line in test.strip().splitlines(): | |||||
| if line.startswith("name:"): | |||||
| name = line[len("name:") :].strip() | |||||
| elif line.startswith("label:"): | |||||
| label = line[len("label:") :].strip() | |||||
| elif line.startswith("input:"): | |||||
| raw = line[len("input:") :].strip() | |||||
| if raw[0] == '"' and raw[-1] == '"': | |||||
| raw = raw[1:-1] | |||||
| raw = raw.encode("raw_unicode_escape") | |||||
| input = raw.decode("unicode_escape") | |||||
| elif line.startswith("output:"): | |||||
| raw = line[len("output:") :].strip() | |||||
| try: | |||||
| output = eval(raw, dict(vars(tokens))) | |||||
| except Exception as err: | |||||
| raise _TestParseError( | |||||
| f"Could not parse a test in {file_path!r}:\n\t{err}\n\n" | |||||
| f"Raw test:\n{test}" | |||||
| ) | |||||
| if not name: | |||||
| raise _TestParseError( | |||||
| f"Test in {filename!r} lacks a name\nRaw test:\n{test}" | |||||
| ) | |||||
| continue | continue | ||||
| if data["input"] is None or data["output"] is None: | |||||
| error = ( | |||||
| "Test '{}' in '{}' was ignored because it lacked an input or an output" | |||||
| if input is None or output is None: | |||||
| raise _TestParseError( | |||||
| f"Test {name!r} in {file_path!r} lacks an input or an output" | |||||
| ) | ) | ||||
| warnings.warn(error.format(data["name"], filename)) | |||||
| continue | continue | ||||
| if not isinstance(output, list) or not all( | |||||
| isinstance(tok, tokens.Token) for tok in output | |||||
| ): | |||||
| raise _TestParseError( | |||||
| f"Test {name!r} in {file_path!r} should have a list of tokens as its output" | |||||
| ) | |||||
| # Include test filename in name | |||||
| data["name"] = "{}:{}".format(name, data["name"]) | |||||
| yield data | |||||
| yield _TestCase( | |||||
| name=f"{filename}:{name}", | |||||
| label=label, | |||||
| input=input, | |||||
| output=output, | |||||
| ) | |||||
| def build(): | def build(): | ||||
| """Load and install all tests from the 'tokenizer' directory.""" | """Load and install all tests from the 'tokenizer' directory.""" | ||||
| directory = path.join(path.dirname(__file__), "tokenizer") | |||||
| directory = os.path.join(os.path.dirname(__file__), "tokenizer") | |||||
| extension = ".mwtest" | extension = ".mwtest" | ||||
| for filename in listdir(directory): | |||||
| for filename in os.listdir(directory): | |||||
| if not filename.endswith(extension): | if not filename.endswith(extension): | ||||
| continue | continue | ||||
| fullname = path.join(directory, filename) | |||||
| with codecs.open(fullname, "r", encoding="utf8") as fp: | |||||
| fullname = os.path.join(directory, filename) | |||||
| with open(fullname) as fp: | |||||
| text = fp.read() | text = fp.read() | ||||
| name = path.split(fullname)[1][: -len(extension)] | |||||
| name = os.path.split(fullname)[1][: -len(extension)] | |||||
| yield from _load_tests(fullname, name, text) | yield from _load_tests(fullname, name, text) | ||||
| @@ -113,23 +121,22 @@ def build(): | |||||
| filter(None, (CTokenizer, PyTokenizer)), | filter(None, (CTokenizer, PyTokenizer)), | ||||
| ids=lambda t: "CTokenizer" if t.USES_C else "PyTokenizer", | ids=lambda t: "CTokenizer" if t.USES_C else "PyTokenizer", | ||||
| ) | ) | ||||
| @pytest.mark.parametrize("data", build(), ids=lambda data: data["name"]) | |||||
| def test_tokenizer(tokenizer, data): | |||||
| expected = data["output"] | |||||
| actual = tokenizer().tokenize(data["input"]) | |||||
| assert expected == actual | |||||
| @pytest.mark.parametrize("test_case", build(), ids=lambda test_case: test_case.name) | |||||
| def test_tokenizer(tokenizer, test_case: _TestCase): | |||||
| actual = tokenizer().tokenize(test_case.input) | |||||
| assert test_case.output == actual | |||||
| @pytest.mark.parametrize("data", build(), ids=lambda data: data["name"]) | |||||
| def test_roundtrip(data): | |||||
| expected = data["input"] | |||||
| actual = str(Builder().build(data["output"][:])) | |||||
| assert expected == actual | |||||
| @pytest.mark.parametrize("test_case", build(), ids=lambda test_case: test_case.name) | |||||
| def test_roundtrip(test_case: _TestCase): | |||||
| actual = str(Builder().build(test_case.output[:])) | |||||
| assert test_case.input == actual | |||||
| @pytest.mark.skipif(CTokenizer is None, reason="CTokenizer not available") | @pytest.mark.skipif(CTokenizer is None, reason="CTokenizer not available") | ||||
| def test_c_tokenizer_uses_c(): | def test_c_tokenizer_uses_c(): | ||||
| """make sure the C tokenizer identifies as using a C extension""" | """make sure the C tokenizer identifies as using a C extension""" | ||||
| assert CTokenizer is not None | |||||
| assert CTokenizer.USES_C is True | assert CTokenizer.USES_C is True | ||||
| assert CTokenizer().USES_C is True | assert CTokenizer().USES_C is True | ||||
| @@ -96,4 +96,4 @@ def test_equality(): | |||||
| ) | ) | ||||
| def test_repr_equality(token): | def test_repr_equality(token): | ||||
| """check that eval(repr(token)) == token""" | """check that eval(repr(token)) == token""" | ||||
| assert token == eval(repr(token), vars(tokens)) | |||||
| assert token == eval(repr(token), dict(vars(tokens))) | |||||