diff --git a/.coveragerc b/.coveragerc index 0a92f19..909a0e2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,3 +2,7 @@ exclude_lines = pragma: no cover raise NotImplementedError() +partial_branches = + pragma: no branch + if py3k: + if not py3k: diff --git a/CHANGELOG b/CHANGELOG index d733cee..1200575 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,10 +10,15 @@ v0.4 (unreleased): option, RECURSE_OTHERS, which recurses over all children except instances of 'forcetype' (for example, `code.filter_templates(code.RECURSE_OTHERS)` returns all un-nested templates). +- Calling Template.remove() with a Parameter object that is not part of the + template now raises ValueError instead of doing nothing. +- Parameters with non-integer keys can no longer be created with + 'showkey=False', nor have the value of this attribute be set to False later. - If something goes wrong while parsing, ParserError will now be raised. Previously, the parser would produce an unclear BadRoute exception or allow an incorrect node tree to be build. - Fixed a parser bug involving nested tags. +- Test coverage has been improved, and some minor related bugs have been fixed. - Updated and fixed some documentation. v0.3.3 (released April 22, 2014): diff --git a/docs/changelog.rst b/docs/changelog.rst index a530733..ba26722 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,10 +18,17 @@ Unreleased which recurses over all children except instances of *forcetype* (for example, ``code.filter_templates(code.RECURSE_OTHERS)`` returns all un-nested templates). +- Calling :py:meth:`.Template.remove` with a :py:class:`.Parameter` object that + is not part of the template now raises :py:exc:`ValueError` instead of doing + nothing. +- :py:class:`.Parameter`\ s with non-integer keys can no longer be created with + *showkey=False*, nor have the value of this attribute be set to *False* + later. - If something goes wrong while parsing, :py:exc:`.ParserError` will now be raised. Previously, the parser would produce an unclear :py:exc:`.BadRoute` exception or allow an incorrect node tree to be build. - Fixed a parser bug involving nested tags. +- Test coverage has been improved, and some minor related bugs have been fixed. - Updated and fixed some documentation. v0.3.3 diff --git a/mwparserfromhell/nodes/extras/parameter.py b/mwparserfromhell/nodes/extras/parameter.py index e273af9..5a67ae0 100644 --- a/mwparserfromhell/nodes/extras/parameter.py +++ b/mwparserfromhell/nodes/extras/parameter.py @@ -21,6 +21,7 @@ # SOFTWARE. from __future__ import unicode_literals +import re from ...compat import str from ...string_mixin import StringMixIn @@ -39,6 +40,8 @@ class Parameter(StringMixIn): def __init__(self, name, value, showkey=True): super(Parameter, self).__init__() + if not showkey and not self.can_hide_key(name): + raise ValueError("key {0!r} cannot be hidden".format(name)) self._name = name self._value = value self._showkey = showkey @@ -48,6 +51,11 @@ class Parameter(StringMixIn): return str(self.name) + "=" + str(self.value) return str(self.value) + @staticmethod + def can_hide_key(key): + """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): """The name of the parameter as a :py:class:`~.Wikicode` object.""" @@ -73,4 +81,7 @@ class Parameter(StringMixIn): @showkey.setter def showkey(self, newval): - self._showkey = bool(newval) + newval = bool(newval) + if not newval and not self.can_hide_key(self.name): + raise ValueError("parameter key cannot be hidden") + self._showkey = newval diff --git a/mwparserfromhell/nodes/html_entity.py b/mwparserfromhell/nodes/html_entity.py index c75cb99..95f1492 100644 --- a/mwparserfromhell/nodes/html_entity.py +++ b/mwparserfromhell/nodes/html_entity.py @@ -77,17 +77,17 @@ class HTMLEntity(Node): # Test whether we're on the wide or narrow Python build. Check # the length of a non-BMP code point # (U+1F64A, SPEAK-NO-EVIL MONKEY): - if len("\U0001F64A") == 2: - # Ensure this is within the range we can encode: - if value > 0x10FFFF: - raise ValueError("unichr() arg not in range(0x110000)") - code = value - 0x10000 - if value < 0: # Invalid code point - raise - lead = 0xD800 + (code >> 10) - trail = 0xDC00 + (code % (1 << 10)) - return unichr(lead) + unichr(trail) - raise + if len("\U0001F64A") == 1: # pragma: no cover + raise + # Ensure this is within the range we can encode: + if value > 0x10FFFF: + raise ValueError("unichr() arg not in range(0x110000)") + code = value - 0x10000 + if value < 0: # Invalid code point + raise + lead = 0xD800 + (code >> 10) + trail = 0xDC00 + (code % (1 << 10)) + return unichr(lead) + unichr(trail) @property def value(self): diff --git a/mwparserfromhell/nodes/template.py b/mwparserfromhell/nodes/template.py index 3b5b35c..c0fda5d 100644 --- a/mwparserfromhell/nodes/template.py +++ b/mwparserfromhell/nodes/template.py @@ -155,6 +155,7 @@ class Template(Node): else: self.params.pop(i) return + raise ValueError(needle) @property def name(self): @@ -254,21 +255,19 @@ class Template(Node): return existing if showkey is None: - try: + if Parameter.can_hide_key(name): int_name = int(str(name)) - except ValueError: - showkey = True - else: int_keys = set() for param in self.params: if not param.showkey: - if re.match(r"[1-9][0-9]*$", param.name.strip()): - int_keys.add(int(str(param.name))) + int_keys.add(int(str(param.name))) expected = min(set(range(1, len(int_keys) + 2)) - int_keys) if expected == int_name: showkey = False else: showkey = True + else: + showkey = True if not showkey: self._surface_escape(value, "=") diff --git a/tests/test_builder.py b/tests/test_builder.py index ed306f7..58e3d1e 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -27,6 +27,7 @@ try: except ImportError: import unittest +from mwparserfromhell.compat import py3k from mwparserfromhell.nodes import (Argument, Comment, ExternalLink, Heading, HTMLEntity, Tag, Template, Text, Wikilink) from mwparserfromhell.nodes.extras import Attribute, Parameter @@ -422,9 +423,9 @@ class TestBuilder(TreeEqualityTestCase): def test_parser_error(self): """test whether ParserError gets thrown for bad input""" + func = self.assertRaisesRegex if py3k else self.assertRaisesRegexp msg = r"_handle_token\(\) got unexpected TemplateClose" - self.assertRaisesRegexp( - ParserError, msg, self.builder.build, [tokens.TemplateClose()]) + func(ParserError, msg, self.builder.build, [tokens.TemplateClose()]) if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/tests/test_html_entity.py b/tests/test_html_entity.py index eb6f606..3df596a 100644 --- a/tests/test_html_entity.py +++ b/tests/test_html_entity.py @@ -108,6 +108,7 @@ class TestHTMLEntity(TreeEqualityTestCase): self.assertRaises(ValueError, setattr, node3, "value", -1) self.assertRaises(ValueError, setattr, node1, "value", 110000) self.assertRaises(ValueError, setattr, node1, "value", "1114112") + self.assertRaises(ValueError, setattr, node1, "value", "12FFFF") def test_named(self): """test getter/setter for the named attribute""" @@ -163,10 +164,14 @@ class TestHTMLEntity(TreeEqualityTestCase): node2 = HTMLEntity("107") node3 = HTMLEntity("e9") node4 = HTMLEntity("1f648") + node5 = HTMLEntity("-2") + node6 = HTMLEntity("110000", named=False, hexadecimal=True) self.assertEqual("\xa0", node1.normalize()) self.assertEqual("k", node2.normalize()) self.assertEqual("é", node3.normalize()) self.assertEqual("\U0001F648", node4.normalize()) + self.assertRaises(ValueError, node5.normalize) + self.assertRaises(ValueError, node6.normalize) if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/tests/test_parameter.py b/tests/test_parameter.py index ee52b59..2a4bb75 100644 --- a/tests/test_parameter.py +++ b/tests/test_parameter.py @@ -71,9 +71,10 @@ class TestParameter(TreeEqualityTestCase): self.assertFalse(node1.showkey) self.assertTrue(node2.showkey) node1.showkey = True - node2.showkey = "" self.assertTrue(node1.showkey) - self.assertFalse(node2.showkey) + node1.showkey = "" + self.assertFalse(node1.showkey) + self.assertRaises(ValueError, setattr, node2, "showkey", False) if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/tests/test_tag.py b/tests/test_tag.py index 111511a..0eae713 100644 --- a/tests/test_tag.py +++ b/tests/test_tag.py @@ -33,6 +33,7 @@ from mwparserfromhell.nodes.extras import Attribute from ._test_tree_equality import TreeEqualityTestCase, wrap, wraptext agen = lambda name, value: Attribute(wraptext(name), wraptext(value)) +agennv = lambda name: Attribute(wraptext(name)) agennq = lambda name, value: Attribute(wraptext(name), wraptext(value), False) agenp = lambda name, v, a, b, c: Attribute(wraptext(name), v, True, a, b, c) agenpnv = lambda name, a, b, c: Attribute(wraptext(name), None, True, a, b, c) @@ -74,10 +75,10 @@ class TestTag(TreeEqualityTestCase): node1 = Tag(wraptext("ref"), wraptext("foobar")) # '''bold text''' node2 = Tag(wraptext("b"), wraptext("bold text"), wiki_markup="'''") - # + # node3 = Tag(wraptext("img"), - attrs=[Attribute(wraptext("id"), wraptext("foo")), - Attribute(wraptext("class"), wraptext("bar"))], + attrs=[agen("id", "foo"), agen("class", "bar"), + agennv("selected")], self_closing=True, padding=" ") gen1 = node1.__children__() @@ -89,6 +90,7 @@ class TestTag(TreeEqualityTestCase): self.assertEqual(node3.attributes[0].value, next(gen3)) self.assertEqual(node3.attributes[1].name, next(gen3)) self.assertEqual(node3.attributes[1].value, next(gen3)) + self.assertEqual(node3.attributes[2].name, next(gen3)) self.assertEqual(node1.contents, next(gen1)) self.assertEqual(node2.contents, next(gen2)) self.assertEqual(node1.closing_tag, next(gen1)) @@ -113,7 +115,8 @@ class TestTag(TreeEqualityTestCase): getter, marker = object(), object() get = lambda code: output.append((getter, code)) mark = lambda: output.append(marker) - node1 = Tag(wraptext("ref"), wraptext("text"), [agen("name", "foo")]) + node1 = Tag(wraptext("ref"), wraptext("text"), + [agen("name", "foo"), agennv("selected")]) node2 = Tag(wraptext("br"), self_closing=True, padding=" ") node3 = Tag(wraptext("br"), self_closing=True, invalid=True, implicit=True, padding=" ") @@ -122,9 +125,10 @@ class TestTag(TreeEqualityTestCase): node3.__showtree__(output.append, get, mark) valid = [ "<", (getter, node1.tag), (getter, node1.attributes[0].name), - " = ", marker, (getter, node1.attributes[0].value), ">", - (getter, node1.contents), "", - "<", (getter, node2.tag), "/>", ""] + " = ", marker, (getter, node1.attributes[0].value), + (getter, node1.attributes[1].name), ">", (getter, node1.contents), + "", "<", (getter, node2.tag), + "/>", ""] self.assertEqual(valid, output) def test_tag(self): diff --git a/tests/test_template.py b/tests/test_template.py index 584b02f..e015a6a 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -130,6 +130,8 @@ class TestTemplate(TreeEqualityTestCase): self.assertTrue(node4.has("b", False)) self.assertTrue(node3.has("b", True)) self.assertFalse(node4.has("b", True)) + self.assertFalse(node1.has_param("foobar", False)) + self.assertTrue(node2.has_param(1, False)) def test_get(self): """test Template.get()""" @@ -176,52 +178,41 @@ class TestTemplate(TreeEqualityTestCase): pgens("b ", " c\n"), pgens("\nd ", " e"), pgens("\nf ", "g ")]) node16 = Template(wraptext("a"), [ pgens("\nb ", " c"), pgens("\nd ", " e"), pgens("\nf ", " g")]) - node17 = Template(wraptext("a"), [ - pgens("\nb ", " c"), pgens("\nd ", " e"), pgens("\nf ", " g")]) - node18 = Template(wraptext("a\n"), [ - pgens("b ", "c\n"), pgens("d ", " e"), pgens("f ", "g\n"), - pgens("h ", " i\n")]) - node19 = Template(wraptext("a"), [ - pgens("b ", " c\n"), pgens("\nd ", " e"), pgens("\nf ", "g ")]) - node20 = Template(wraptext("a"), [ - pgens("\nb ", " c"), pgens("\nd ", " e"), pgens("\nf ", " g")]) - node21 = Template(wraptext("a"), [pgenh("1", "b")]) - node22 = Template(wraptext("a"), [pgenh("1", "b")]) - node23 = Template(wraptext("a"), [pgenh("1", "b")]) - node24 = Template(wraptext("a"), [pgenh("1", "b"), pgenh("2", "c"), + node17 = Template(wraptext("a"), [pgenh("1", "b")]) + node18 = Template(wraptext("a"), [pgenh("1", "b")]) + node19 = Template(wraptext("a"), [pgenh("1", "b")]) + node20 = Template(wraptext("a"), [pgenh("1", "b"), pgenh("2", "c"), pgenh("3", "d"), pgenh("4", "e")]) - node25 = Template(wraptext("a"), [pgenh("1", "b"), pgenh("2", "c"), + node21 = Template(wraptext("a"), [pgenh("1", "b"), pgenh("2", "c"), pgens("4", "d"), pgens("5", "e")]) - node26 = Template(wraptext("a"), [pgenh("1", "b"), pgenh("2", "c"), + node22 = Template(wraptext("a"), [pgenh("1", "b"), pgenh("2", "c"), pgens("4", "d"), pgens("5", "e")]) + node23 = Template(wraptext("a"), [pgenh("1", "b")]) + node24 = Template(wraptext("a"), [pgenh("1", "b")]) + node25 = Template(wraptext("a"), [pgens("b", "c")]) + node26 = Template(wraptext("a"), [pgenh("1", "b")]) node27 = Template(wraptext("a"), [pgenh("1", "b")]) - node28 = Template(wraptext("a"), [pgenh("1", "b")]) - node29 = Template(wraptext("a"), [pgens("b", "c")]) - node30 = Template(wraptext("a"), [pgenh("1", "b")]) - node31 = Template(wraptext("a"), [pgenh("1", "b")]) - node32 = Template(wraptext("a"), [pgens("1", "b")]) - node33 = Template(wraptext("a"), [ + node28 = Template(wraptext("a"), [pgens("1", "b")]) + node29 = Template(wraptext("a"), [ pgens("\nb ", " c"), pgens("\nd ", " e"), pgens("\nf ", " g")]) - node34 = Template(wraptext("a\n"), [ + node30 = Template(wraptext("a\n"), [ pgens("b ", "c\n"), pgens("d ", " e"), pgens("f ", "g\n"), pgens("h ", " i\n")]) - node35 = Template(wraptext("a"), [ + node31 = Template(wraptext("a"), [ pgens("b ", " c\n"), pgens("\nd ", " e"), pgens("\nf ", "g ")]) - node36 = Template(wraptext("a"), [ + node32 = Template(wraptext("a"), [ pgens("\nb ", " c "), pgens("\nd ", " e "), pgens("\nf ", " g ")]) - node37 = Template(wraptext("a"), [pgens("b", "c"), pgens("d", "e"), - pgens("b", "f"), pgens("b", "h"), - pgens("i", "j")]) - node37 = Template(wraptext("a"), [pgens("b", "c"), pgens("d", "e"), + node33 = Template(wraptext("a"), [pgens("b", "c"), pgens("d", "e"), pgens("b", "f"), pgens("b", "h"), pgens("i", "j")]) - node38 = Template(wraptext("a"), [pgens("1", "b"), pgens("x", "y"), + node34 = Template(wraptext("a"), [pgens("1", "b"), pgens("x", "y"), pgens("1", "c"), pgens("2", "d")]) - node39 = Template(wraptext("a"), [pgens("1", "b"), pgens("x", "y"), + node35 = Template(wraptext("a"), [pgens("1", "b"), pgens("x", "y"), pgenh("1", "c"), pgenh("2", "d")]) - node40 = Template(wraptext("a"), [pgens("b", "c"), pgens("d", "e"), + node36 = Template(wraptext("a"), [pgens("b", "c"), pgens("d", "e"), pgens("f", "g")]) - node41 = Template(wraptext("a"), [pgenh("1", "")]) + node37 = Template(wraptext("a"), [pgenh("1", "")]) + node38 = Template(wraptext("abc")) node1.add("e", "f", showkey=True) node2.add(2, "g", showkey=False) @@ -241,31 +232,29 @@ class TestTemplate(TreeEqualityTestCase): node14.add("j", "k", showkey=True) node15.add("h", "i", showkey=True) node16.add("h", "i", showkey=True, preserve_spacing=False) - node17.add("h", "i", showkey=False) - node18.add("j", "k", showkey=False) - node19.add("h", "i", showkey=False) - node20.add("h", "i", showkey=False, preserve_spacing=False) - node21.add("2", "c") - node22.add("3", "c") - node23.add("c", "d") - node24.add("5", "f") - node25.add("3", "f") - node26.add("6", "f") - node27.add("c", "foo=bar") - node28.add("2", "foo=bar") - node29.add("b", "d") - node30.add("1", "foo=bar") - node31.add("1", "foo=bar", showkey=True) - node32.add("1", "foo=bar", showkey=False) - node33.add("d", "foo") - node34.add("f", "foo") - node35.add("f", "foo") - node36.add("d", "foo", preserve_spacing=False) - node37.add("b", "k") - node38.add("1", "e") - node39.add("1", "e") - node40.add("d", "h", before="b") - node41.add(1, "b") + node17.add("2", "c") + node18.add("3", "c") + node19.add("c", "d") + node20.add("5", "f") + node21.add("3", "f") + node22.add("6", "f") + node23.add("c", "foo=bar") + node24.add("2", "foo=bar") + node25.add("b", "d") + node26.add("1", "foo=bar") + node27.add("1", "foo=bar", showkey=True) + node28.add("1", "foo=bar", showkey=False) + node29.add("d", "foo") + node30.add("f", "foo") + node31.add("f", "foo") + node32.add("d", "foo", preserve_spacing=False) + node33.add("b", "k") + node34.add("1", "e") + node35.add("1", "e") + node36.add("d", "h", before="b") + node37.add(1, "b") + node38.add("1", "foo") + self.assertRaises(ValueError, node38.add, "z", "bar", showkey=False) self.assertEqual("{{a|b=c|d|e=f}}", node1) self.assertEqual("{{a|b=c|d|g}}", node2) @@ -285,34 +274,31 @@ class TestTemplate(TreeEqualityTestCase): self.assertEqual("{{a\n|b =c\n|d = e|f =g\n|h = i\n|j =k\n}}", node14) self.assertEqual("{{a|b = c\n|\nd = e|\nf =g |h =i}}", node15) self.assertEqual("{{a|\nb = c|\nd = e|\nf = g|h=i}}", node16) - self.assertEqual("{{a|\nb = c|\nd = e|\nf = g| i}}", node17) - self.assertEqual("{{a\n|b =c\n|d = e|f =g\n|h = i\n|k\n}}", node18) - self.assertEqual("{{a|b = c\n|\nd = e|\nf =g |i}}", node19) - self.assertEqual("{{a|\nb = c|\nd = e|\nf = g|i}}", node20) - self.assertEqual("{{a|b|c}}", node21) - self.assertEqual("{{a|b|3=c}}", node22) - self.assertEqual("{{a|b|c=d}}", node23) - self.assertEqual("{{a|b|c|d|e|f}}", node24) - self.assertEqual("{{a|b|c|4=d|5=e|f}}", node25) - self.assertEqual("{{a|b|c|4=d|5=e|6=f}}", node26) - self.assertEqual("{{a|b|c=foo=bar}}", node27) - self.assertEqual("{{a|b|foo=bar}}", node28) - self.assertIsInstance(node28.params[1].value.get(1), HTMLEntity) - self.assertEqual("{{a|b=d}}", node29) - self.assertEqual("{{a|foo=bar}}", node30) - self.assertIsInstance(node30.params[0].value.get(1), HTMLEntity) - self.assertEqual("{{a|1=foo=bar}}", node31) - self.assertEqual("{{a|foo=bar}}", node32) - self.assertIsInstance(node32.params[0].value.get(1), HTMLEntity) - self.assertEqual("{{a|\nb = c|\nd = foo|\nf = g}}", node33) - self.assertEqual("{{a\n|b =c\n|d = e|f =foo\n|h = i\n}}", node34) - self.assertEqual("{{a|b = c\n|\nd = e|\nf =foo }}", node35) - self.assertEqual("{{a|\nb = c |\nd =foo|\nf = g }}", node36) - self.assertEqual("{{a|b=k|d=e|i=j}}", node37) - self.assertEqual("{{a|1=e|x=y|2=d}}", node38) - self.assertEqual("{{a|x=y|e|d}}", node39) - self.assertEqual("{{a|b=c|d=h|f=g}}", node40) - self.assertEqual("{{a|b}}", node41) + self.assertEqual("{{a|b|c}}", node17) + self.assertEqual("{{a|b|3=c}}", node18) + self.assertEqual("{{a|b|c=d}}", node19) + self.assertEqual("{{a|b|c|d|e|f}}", node20) + self.assertEqual("{{a|b|c|4=d|5=e|f}}", node21) + self.assertEqual("{{a|b|c|4=d|5=e|6=f}}", node22) + self.assertEqual("{{a|b|c=foo=bar}}", node23) + self.assertEqual("{{a|b|foo=bar}}", node24) + self.assertIsInstance(node24.params[1].value.get(1), HTMLEntity) + self.assertEqual("{{a|b=d}}", node25) + self.assertEqual("{{a|foo=bar}}", node26) + self.assertIsInstance(node26.params[0].value.get(1), HTMLEntity) + self.assertEqual("{{a|1=foo=bar}}", node27) + self.assertEqual("{{a|foo=bar}}", node28) + self.assertIsInstance(node28.params[0].value.get(1), HTMLEntity) + self.assertEqual("{{a|\nb = c|\nd = foo|\nf = g}}", node29) + self.assertEqual("{{a\n|b =c\n|d = e|f =foo\n|h = i\n}}", node30) + self.assertEqual("{{a|b = c\n|\nd = e|\nf =foo }}", node31) + self.assertEqual("{{a|\nb = c |\nd =foo|\nf = g }}", node32) + self.assertEqual("{{a|b=k|d=e|i=j}}", node33) + self.assertEqual("{{a|1=e|x=y|2=d}}", node34) + self.assertEqual("{{a|x=y|e|d}}", node35) + self.assertEqual("{{a|b=c|d=h|f=g}}", node36) + self.assertEqual("{{a|b}}", node37) + self.assertEqual("{{abc|foo}}", node38) def test_remove(self): """test Template.remove()""" @@ -373,6 +359,8 @@ class TestTemplate(TreeEqualityTestCase): node26 = Template(wraptext("foo"), [ pgens("a", "b"), pgens("c", "d"), pgens("e", "f"), pgens("a", "b"), pgens("a", "b")]) + node27 = Template(wraptext("foo"), [pgenh("1", "bar")]) + node28 = Template(wraptext("foo"), [pgenh("1", "bar")]) node2.remove("1") node2.remove("abc") @@ -430,6 +418,7 @@ class TestTemplate(TreeEqualityTestCase): self.assertEqual("{{foo|a=|c=d|e=f|a=b|a=b}}", node24) self.assertEqual("{{foo|a=b|c=d|e=f|a=b}}", node25) self.assertEqual("{{foo|a=b|c=d|e=f|a=|a=b}}", node26) + self.assertRaises(ValueError, node27.remove, node28.get(1)) if __name__ == "__main__": unittest.main(verbosity=2)