diff --git a/.travis.yml b/.travis.yml index 7a9920d..347badd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: - "2.7" + - "3.2" - "3.3" install: python setup.py build script: python setup.py test -q diff --git a/CHANGELOG b/CHANGELOG index 4663700..99eff38 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,9 @@ +v0.3.2 (released September 1, 2013): + +- Added support for Python 3.2 (along with current support for 3.3 and 2.7). +- Renamed Template.remove()'s first argument from 'name' to 'param', which now + accepts Parameter objects in addition to parameter name strings. + v0.3.1 (released August 29, 2013): - Fixed a parser bug involving URLs nested inside other markup. diff --git a/docs/changelog.rst b/docs/changelog.rst index 3546f0c..e72baef 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,17 @@ Changelog ========= +v0.3.2 +------ + +`Released September 1, 2013 `_ +(`changes `__): + +- Added support for Python 3.2 (along with current support for 3.3 and 2.7). +- Renamed :py:meth:`.Template.remove`\ 's first argument from *name* to + *param*, which now accepts :py:class:`.Parameter` objects in addition to + parameter name strings. + v0.3.1 ------ diff --git a/mwparserfromhell/__init__.py b/mwparserfromhell/__init__.py index a5fda7c..6569d96 100644 --- a/mwparserfromhell/__init__.py +++ b/mwparserfromhell/__init__.py @@ -31,7 +31,7 @@ from __future__ import unicode_literals __author__ = "Ben Kurtovic" __copyright__ = "Copyright (C) 2012, 2013 Ben Kurtovic" __license__ = "MIT License" -__version__ = "0.3.1" +__version__ = "0.3.2" __email__ = "ben.kurtovic@verizon.net" from . import (compat, definitions, nodes, parser, smart_list, string_mixin, diff --git a/mwparserfromhell/compat.py b/mwparserfromhell/compat.py index 864605c..a142128 100644 --- a/mwparserfromhell/compat.py +++ b/mwparserfromhell/compat.py @@ -10,7 +10,8 @@ types are meant to be imported directly from within the parser's modules. import sys -py3k = sys.version_info[0] == 3 +py3k = sys.version_info.major == 3 +py32 = py3k and sys.version_info.minor == 2 if py3k: bytes = bytes diff --git a/mwparserfromhell/nodes/template.py b/mwparserfromhell/nodes/template.py index a6b1665..1b4e3fa 100644 --- a/mwparserfromhell/nodes/template.py +++ b/mwparserfromhell/nodes/template.py @@ -150,6 +150,16 @@ class Template(Node): return False return True + def _remove_exact(self, needle, keep_field): + """Remove a specific parameter, *needle*, from the template.""" + for i, param in enumerate(self.params): + if param is needle: + if keep_field or not self._remove_without_field(param, i): + self._blank_param_value(param.value) + else: + self.params.pop(i) + return + @property def name(self): """The name of the template, as a :py:class:`~.Wikicode` object.""" @@ -180,7 +190,8 @@ class Template(Node): return True return False - has_param = lambda self, *args, **kwargs: self.has(*args, **kwargs) + has_param = lambda self, name, ignore_empty=True: \ + self.has(name, ignore_empty) has_param.__doc__ = "Alias for :py:meth:`has`." def get(self, name): @@ -280,8 +291,12 @@ class Template(Node): self.params.append(param) return param - def remove(self, name, keep_field=False): - """Remove a parameter from the template whose name is *name*. + def remove(self, param, keep_field=False): + """Remove a parameter from the template, identified by *param*. + + If *param* is a :py:class:`.Parameter` object, it will be matched + exactly, otherwise it will be treated like the *name* argument to + :py:meth:`has` and :py:meth:`get`. If *keep_field* is ``True``, we will keep the parameter's name, but blank its value. Otherwise, we will remove the parameter completely @@ -289,12 +304,14 @@ class Template(Node): from ``{{foo|bar|baz}}`` is unsafe because ``{{foo|baz}}`` is not what we expected, so ``{{foo||baz}}`` will be produced instead). - If the parameter shows up multiple times in the template, we will - remove all instances of it (and keep one if *keep_field* is ``True`` - - the first instance if none have dependents, otherwise the one with - dependents will be kept). + If the parameter shows up multiple times in the template and *param* is + not a :py:class:`.Parameter` object, we will remove all instances of it + (and keep only one if *keep_field* is ``True`` - the first instance if + none have dependents, otherwise the one with dependents will be kept). """ - name = str(name).strip() + if isinstance(param, Parameter): + return self._remove_exact(param, keep_field) + name = str(param).strip() removed = False to_remove = [] for i, param in enumerate(self.params): @@ -304,15 +321,15 @@ class Template(Node): self._blank_param_value(param.value) keep_field = False else: - to_remove.append(param) + to_remove.append(i) else: if self._remove_without_field(param, i): - to_remove.append(param) + to_remove.append(i) else: self._blank_param_value(param.value) if not removed: removed = True if not removed: raise ValueError(name) - for param in to_remove: - self.params.remove(param) + for i in reversed(to_remove): + self.params.pop(i) diff --git a/mwparserfromhell/string_mixin.py b/mwparserfromhell/string_mixin.py index a406401..c52d4ca 100644 --- a/mwparserfromhell/string_mixin.py +++ b/mwparserfromhell/string_mixin.py @@ -27,7 +27,7 @@ interface for the ``unicode`` type (``str`` on py3k) in a dynamic manner. from __future__ import unicode_literals -from .compat import py3k, str +from .compat import py3k, py32, str __all__ = ["StringMixIn"] @@ -125,7 +125,7 @@ class StringMixIn(object): def capitalize(self): return self.__unicode__().capitalize() - if py3k: + if py3k and not py32: @inheritdoc def casefold(self): return self.__unicode__().casefold() @@ -288,7 +288,7 @@ class StringMixIn(object): def rpartition(self, sep): return self.__unicode__().rpartition(sep) - if py3k: + if py3k and not py32: @inheritdoc def rsplit(self, sep=None, maxsplit=None): kwargs = {} @@ -310,7 +310,7 @@ class StringMixIn(object): def rstrip(self, chars=None): return self.__unicode__().rstrip(chars) - if py3k: + if py3k and not py32: @inheritdoc def split(self, sep=None, maxsplit=None): kwargs = {} diff --git a/setup.py b/setup.py index d2ad17d..d6e77a1 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,7 @@ setup( "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", "Topic :: Text Processing :: Markup" ], diff --git a/tests/test_string_mixin.py b/tests/test_string_mixin.py index b829bb2..5ee857c 100644 --- a/tests/test_string_mixin.py +++ b/tests/test_string_mixin.py @@ -25,7 +25,7 @@ from sys import getdefaultencoding from types import GeneratorType import unittest -from mwparserfromhell.compat import bytes, py3k, str +from mwparserfromhell.compat import bytes, py3k, py32, str from mwparserfromhell.string_mixin import StringMixIn from .compat import range @@ -52,8 +52,10 @@ class TestStringMixIn(unittest.TestCase): "rsplit", "rstrip", "split", "splitlines", "startswith", "strip", "swapcase", "title", "translate", "upper", "zfill"] if py3k: - methods.extend(["casefold", "format_map", "isidentifier", - "isprintable", "maketrans"]) + if not py32: + methods.append("casefold") + methods.extend(["format_map", "isidentifier", "isprintable", + "maketrans"]) else: methods.append("decode") for meth in methods: @@ -325,7 +327,7 @@ class TestStringMixIn(unittest.TestCase): self.assertEqual("", str15.lower()) self.assertEqual("foobar", str16.lower()) self.assertEqual("ß", str22.lower()) - if py3k: + if py3k and not py32: self.assertEqual("", str15.casefold()) self.assertEqual("foobar", str16.casefold()) self.assertEqual("ss", str22.casefold()) diff --git a/tests/test_template.py b/tests/test_template.py index 9ed099d..26a2e39 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -316,12 +316,12 @@ class TestTemplate(TreeEqualityTestCase): def test_remove(self): """test Template.remove()""" node1 = Template(wraptext("foobar")) - node2 = Template(wraptext("foo"), [pgenh("1", "bar"), - pgens("abc", "def")]) - node3 = Template(wraptext("foo"), [pgenh("1", "bar"), - pgens("abc", "def")]) - node4 = Template(wraptext("foo"), [pgenh("1", "bar"), - pgenh("2", "baz")]) + node2 = Template(wraptext("foo"), + [pgenh("1", "bar"), pgens("abc", "def")]) + node3 = Template(wraptext("foo"), + [pgenh("1", "bar"), pgens("abc", "def")]) + node4 = Template(wraptext("foo"), + [pgenh("1", "bar"), pgenh("2", "baz")]) node5 = Template(wraptext("foo"), [ pgens(" a", "b"), pgens("b", "c"), pgens("a ", "d")]) node6 = Template(wraptext("foo"), [ @@ -334,6 +334,44 @@ class TestTemplate(TreeEqualityTestCase): pgens("1 ", "a"), pgenh("1", "b"), pgenh("2", "c")]) node10 = Template(wraptext("foo"), [ pgens("1 ", "a"), pgenh("1", "b"), pgenh("2", "c")]) + node11 = Template(wraptext("foo"), [ + pgens(" a", "b"), pgens("b", "c"), pgens("a ", "d")]) + node12 = Template(wraptext("foo"), [ + pgens(" a", "b"), pgens("b", "c"), pgens("a ", "d")]) + node13 = Template(wraptext("foo"), [ + pgens(" a", "b"), pgens("b", "c"), pgens("a ", "d")]) + node14 = Template(wraptext("foo"), [ + pgens(" a", "b"), pgens("b", "c"), pgens("a ", "d")]) + node15 = Template(wraptext("foo"), [ + pgens(" a", "b"), pgens("b", "c"), pgens("a ", "d")]) + node16 = Template(wraptext("foo"), [ + pgens(" a", "b"), pgens("b", "c"), pgens("a ", "d")]) + node17 = Template(wraptext("foo"), [ + pgens("1 ", "a"), pgenh("1", "b"), pgenh("2", "c")]) + node18 = Template(wraptext("foo"), [ + pgens("1 ", "a"), pgenh("1", "b"), pgenh("2", "c")]) + node19 = Template(wraptext("foo"), [ + pgens("1 ", "a"), pgenh("1", "b"), pgenh("2", "c")]) + node20 = Template(wraptext("foo"), [ + pgens("1 ", "a"), pgenh("1", "b"), pgenh("2", "c")]) + node21 = Template(wraptext("foo"), [ + pgens("a", "b"), pgens("c", "d"), pgens("e", "f"), pgens("a", "b"), + pgens("a", "b")]) + node22 = Template(wraptext("foo"), [ + pgens("a", "b"), pgens("c", "d"), pgens("e", "f"), pgens("a", "b"), + pgens("a", "b")]) + node23 = Template(wraptext("foo"), [ + pgens("a", "b"), pgens("c", "d"), pgens("e", "f"), pgens("a", "b"), + pgens("a", "b")]) + node24 = Template(wraptext("foo"), [ + pgens("a", "b"), pgens("c", "d"), pgens("e", "f"), pgens("a", "b"), + pgens("a", "b")]) + node25 = Template(wraptext("foo"), [ + pgens("a", "b"), pgens("c", "d"), pgens("e", "f"), pgens("a", "b"), + pgens("a", "b")]) + node26 = Template(wraptext("foo"), [ + pgens("a", "b"), pgens("c", "d"), pgens("e", "f"), pgens("a", "b"), + pgens("a", "b")]) node2.remove("1") node2.remove("abc") @@ -346,6 +384,22 @@ class TestTemplate(TreeEqualityTestCase): node8.remove(1, keep_field=False) node9.remove(1, keep_field=True) node10.remove(1, keep_field=False) + node11.remove(node11.params[0], keep_field=False) + node12.remove(node12.params[0], keep_field=True) + node13.remove(node13.params[1], keep_field=False) + node14.remove(node14.params[1], keep_field=True) + node15.remove(node15.params[2], keep_field=False) + node16.remove(node16.params[2], keep_field=True) + node17.remove(node17.params[0], keep_field=False) + node18.remove(node18.params[0], keep_field=True) + node19.remove(node19.params[1], keep_field=False) + node20.remove(node20.params[1], keep_field=True) + node21.remove("a", keep_field=False) + node22.remove("a", keep_field=True) + node23.remove(node23.params[0], keep_field=False) + node24.remove(node24.params[0], keep_field=True) + node25.remove(node25.params[3], keep_field=False) + node26.remove(node26.params[3], keep_field=True) self.assertRaises(ValueError, node1.remove, 1) self.assertRaises(ValueError, node1.remove, "a") @@ -359,6 +413,22 @@ class TestTemplate(TreeEqualityTestCase): self.assertEqual("{{foo|2=c}}", node8) self.assertEqual("{{foo||c}}", node9) self.assertEqual("{{foo||c}}", node10) + self.assertEqual("{{foo|b=c|a =d}}", node11) + self.assertEqual("{{foo| a=|b=c|a =d}}", node12) + self.assertEqual("{{foo| a=b|a =d}}", node13) + self.assertEqual("{{foo| a=b|b=|a =d}}", node14) + self.assertEqual("{{foo| a=b|b=c}}", node15) + self.assertEqual("{{foo| a=b|b=c|a =}}", node16) + self.assertEqual("{{foo|b|c}}", node17) + self.assertEqual("{{foo|1 =|b|c}}", node18) + self.assertEqual("{{foo|1 =a||c}}", node19) + self.assertEqual("{{foo|1 =a||c}}", node20) + self.assertEqual("{{foo|c=d|e=f}}", node21) + self.assertEqual("{{foo|a=|c=d|e=f}}", node22) + self.assertEqual("{{foo|c=d|e=f|a=b|a=b}}", node23) + 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) if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/tests/tokenizer/tags.mwtest b/tests/tokenizer/tags.mwtest index a0d7f18..a8ca2f0 100644 --- a/tests/tokenizer/tags.mwtest +++ b/tests/tokenizer/tags.mwtest @@ -470,7 +470,7 @@ output: [TemplateOpen(), Text(text="t1"), TemplateClose(), TagOpenOpen(), Text(t name: unparsable_attributed label: a tag that should not be put through the normal parser; parsed attributes input: "{{t1}}{{t2}}{{t3}}" -output: [TemplateOpen(), Text(text=u't1'), TemplateClose(), TagOpenOpen(), Text(text="nowiki"), TagAttrStart(pad_first=" ", pad_before_eq="", pad_after_eq=""), Text(text="attr"), TagAttrEquals(), Text(text="val"), TagAttrStart(pad_first=" ", pad_before_eq="", pad_after_eq=""), Text(text="attr2"), TagAttrEquals(), TagAttrQuote(), TemplateOpen(), Text(text="val2"), TemplateClose(), TagCloseOpen(padding=""), Text(text="{{t2}}"), TagOpenClose(), Text(text="nowiki"), TagCloseClose(), TemplateOpen(), Text(text="t3"), TemplateClose()] +output: [TemplateOpen(), Text(text="t1"), TemplateClose(), TagOpenOpen(), Text(text="nowiki"), TagAttrStart(pad_first=" ", pad_before_eq="", pad_after_eq=""), Text(text="attr"), TagAttrEquals(), Text(text="val"), TagAttrStart(pad_first=" ", pad_before_eq="", pad_after_eq=""), Text(text="attr2"), TagAttrEquals(), TagAttrQuote(), TemplateOpen(), Text(text="val2"), TemplateClose(), TagCloseOpen(padding=""), Text(text="{{t2}}"), TagOpenClose(), Text(text="nowiki"), TagCloseClose(), TemplateOpen(), Text(text="t3"), TemplateClose()] ---