From 83bcb902b87955e99c9eb244b55b0cb5cc144a1a Mon Sep 17 00:00:00 2001 From: Ben Kurtovic Date: Sat, 2 Mar 2019 21:24:40 -0500 Subject: [PATCH] Support manual construction of Node objects (fixes #214) --- CHANGELOG | 1 + docs/changelog.rst | 2 ++ mwparserfromhell/nodes/argument.py | 6 ++--- mwparserfromhell/nodes/comment.py | 6 ++--- mwparserfromhell/nodes/external_link.py | 8 +++--- mwparserfromhell/nodes/extras/attribute.py | 38 +++++++++++++++------------ mwparserfromhell/nodes/extras/parameter.py | 13 +++++---- mwparserfromhell/nodes/heading.py | 6 ++--- mwparserfromhell/nodes/tag.py | 42 ++++++++++++------------------ mwparserfromhell/nodes/template.py | 6 ++--- mwparserfromhell/nodes/text.py | 4 +-- mwparserfromhell/nodes/wikilink.py | 6 ++--- mwparserfromhell/parser/builder.py | 5 ++-- mwparserfromhell/utils.py | 4 +-- tests/_test_tree_equality.py | 4 +-- tests/test_attribute.py | 2 +- tests/test_builder.py | 8 +++--- 17 files changed, 80 insertions(+), 81 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index cb04142..c27e826 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,6 @@ v0.6 (unreleased): +- Fixed manual construction of Node objects, previously unsupported. (#214) - Fixed Wikicode transformation methods (replace(), remove(), etc.) when passed an empty section as an argument. (#212) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2e4ec9d..d6280dd 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,8 @@ v0.6 Unreleased (`changes `__): +- Fixed manual construction of Node objects, previously unsupported. +(`#214 `_) - Fixed :class:`.Wikicode` transformation methods (:meth:`.Wikicode.replace`, :meth:`.Wikicode.remove`, etc.) when passed an empty section as an argument. (`#212 `_) diff --git a/mwparserfromhell/nodes/argument.py b/mwparserfromhell/nodes/argument.py index 4259a35..2da1467 100644 --- a/mwparserfromhell/nodes/argument.py +++ b/mwparserfromhell/nodes/argument.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -33,8 +33,8 @@ class Argument(Node): def __init__(self, name, default=None): super(Argument, self).__init__() - self._name = name - self._default = default + self.name = name + self.default = default def __unicode__(self): start = "{{{" + str(self.name) diff --git a/mwparserfromhell/nodes/comment.py b/mwparserfromhell/nodes/comment.py index 0d141e9..40224ba 100644 --- a/mwparserfromhell/nodes/comment.py +++ b/mwparserfromhell/nodes/comment.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -32,10 +32,10 @@ class Comment(Node): def __init__(self, contents): super(Comment, self).__init__() - self._contents = contents + self.contents = contents def __unicode__(self): - return "" + return "" @property def contents(self): diff --git a/mwparserfromhell/nodes/external_link.py b/mwparserfromhell/nodes/external_link.py index f2659ab..22b2ef7 100644 --- a/mwparserfromhell/nodes/external_link.py +++ b/mwparserfromhell/nodes/external_link.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -33,9 +33,9 @@ class ExternalLink(Node): def __init__(self, url, title=None, brackets=True): super(ExternalLink, self).__init__() - self._url = url - self._title = title - self._brackets = brackets + self.url = url + self.title = title + self.brackets = brackets def __unicode__(self): if self.brackets: diff --git a/mwparserfromhell/nodes/extras/attribute.py b/mwparserfromhell/nodes/extras/attribute.py index 59473c4..4312199 100644 --- a/mwparserfromhell/nodes/extras/attribute.py +++ b/mwparserfromhell/nodes/extras/attribute.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -37,16 +37,15 @@ class Attribute(StringMixIn): """ def __init__(self, name, value=None, quotes='"', pad_first=" ", - pad_before_eq="", pad_after_eq="", check_quotes=True): + pad_before_eq="", pad_after_eq=""): super(Attribute, self).__init__() - if check_quotes and not quotes and self._value_needs_quotes(value): - raise ValueError("given value {!r} requires quotes".format(value)) - self._name = name - self._value = value - self._quotes = quotes - self._pad_first = pad_first - self._pad_before_eq = pad_before_eq - self._pad_after_eq = pad_after_eq + self.name = name + self._quotes = None + self.value = value + self.quotes = quotes + self.pad_first = pad_first + self.pad_before_eq = pad_before_eq + self.pad_after_eq = pad_after_eq def __unicode__(self): result = self.pad_first + str(self.name) + self.pad_before_eq @@ -59,10 +58,17 @@ class Attribute(StringMixIn): @staticmethod def _value_needs_quotes(val): - """Return the preferred quotes for the given value, or None.""" - if val and any(char.isspace() for char in val): - return ('"' in val and "'" in val) or ("'" if '"' in val else '"') - return None + """Return valid quotes for the given value, or None if unneeded.""" + if not val: + return None + val = "".join(str(node) for node in val.filter_text(recursive=False)) + if not any(char.isspace() for char in val): + return None + if "'" in val and '"' not in val: + return '"' + if '"' in val and "'" not in val: + return "'" + return "\"'" # Either acceptable, " preferred over ' def _set_padding(self, attr, value): """Setter for the value of a padding attribute.""" @@ -123,8 +129,8 @@ class Attribute(StringMixIn): else: code = parse_anything(newval) quotes = self._value_needs_quotes(code) - if quotes in ['"', "'"] or (quotes is True and not self.quotes): - self._quotes = quotes + if quotes and (not self.quotes or self.quotes not in quotes): + self._quotes = quotes[0] self._value = code @quotes.setter diff --git a/mwparserfromhell/nodes/extras/parameter.py b/mwparserfromhell/nodes/extras/parameter.py index dff8492..fb0aac0 100644 --- a/mwparserfromhell/nodes/extras/parameter.py +++ b/mwparserfromhell/nodes/extras/parameter.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -40,11 +40,9 @@ 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 {!r} cannot be hidden".format(name)) - self._name = name - self._value = value - self._showkey = showkey + self.name = name + self.value = value + self.showkey = showkey def __unicode__(self): if self.showkey: @@ -83,5 +81,6 @@ class Parameter(StringMixIn): def showkey(self, newval): newval = bool(newval) if not newval and not self.can_hide_key(self.name): - raise ValueError("parameter key cannot be hidden") + raise ValueError("parameter key {!r} cannot be hidden".format( + self.name)) self._showkey = newval diff --git a/mwparserfromhell/nodes/heading.py b/mwparserfromhell/nodes/heading.py index 79f3364..426e742 100644 --- a/mwparserfromhell/nodes/heading.py +++ b/mwparserfromhell/nodes/heading.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -33,8 +33,8 @@ class Heading(Node): def __init__(self, title, level): super(Heading, self).__init__() - self._title = title - self._level = level + self.title = title + self.level = level def __unicode__(self): return ("=" * self.level) + str(self.title) + ("=" * self.level) diff --git a/mwparserfromhell/nodes/tag.py b/mwparserfromhell/nodes/tag.py index f0611a6..c6b88e3 100644 --- a/mwparserfromhell/nodes/tag.py +++ b/mwparserfromhell/nodes/tag.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -38,28 +38,20 @@ class Tag(Node): closing_tag=None, wiki_style_separator=None, closing_wiki_markup=None): super(Tag, self).__init__() - self._tag = tag - if contents is None and not self_closing: - self._contents = parse_anything("") - else: - self._contents = contents + self.tag = tag + self.contents = contents self._attrs = attrs if attrs else [] - self._wiki_markup = wiki_markup - self._self_closing = self_closing - self._invalid = invalid - self._implicit = implicit - self._padding = padding - if closing_tag: - self._closing_tag = closing_tag - else: - self._closing_tag = tag - self._wiki_style_separator = wiki_style_separator + self._closing_wiki_markup = None + self.wiki_markup = wiki_markup + self.self_closing = self_closing + self.invalid = invalid + self.implicit = implicit + self.padding = padding + if closing_tag is not None: + self.closing_tag = closing_tag + self.wiki_style_separator = wiki_style_separator if closing_wiki_markup is not None: - self._closing_wiki_markup = closing_wiki_markup - elif wiki_markup and not self_closing: - self._closing_wiki_markup = wiki_markup - else: - self._closing_wiki_markup = None + self.closing_wiki_markup = closing_wiki_markup def __unicode__(self): if self.wiki_markup: @@ -69,10 +61,10 @@ class Tag(Node): attrs = "" padding = self.padding or "" separator = self.wiki_style_separator or "" - close = self.closing_wiki_markup or "" if self.self_closing: return self.wiki_markup + attrs + padding + separator else: + close = self.closing_wiki_markup or "" return self.wiki_markup + attrs + padding + separator + \ str(self.contents) + close @@ -93,10 +85,10 @@ class Tag(Node): yield attr.name if attr.value is not None: yield attr.value - if self.contents: + if not self.self_closing: yield self.contents - if not self.self_closing and not self.wiki_markup and self.closing_tag: - yield self.closing_tag + if not self.wiki_markup and self.closing_tag: + yield self.closing_tag def __strip__(self, **kwargs): if self.contents and is_visible(self.tag): diff --git a/mwparserfromhell/nodes/template.py b/mwparserfromhell/nodes/template.py index 9c058d4..11bccc4 100644 --- a/mwparserfromhell/nodes/template.py +++ b/mwparserfromhell/nodes/template.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2017 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -38,7 +38,7 @@ class Template(Node): def __init__(self, name, params=None): super(Template, self).__init__() - self._name = name + self.name = name if params: self._params = params else: @@ -108,7 +108,7 @@ class Template(Node): def _blank_param_value(value): """Remove the content from *value* while keeping its whitespace. - Replace *value*\ 's nodes with two text nodes, the first containing + Replace *value*\\ 's nodes with two text nodes, the first containing whitespace from before its content and the second containing whitespace from after its content. """ diff --git a/mwparserfromhell/nodes/text.py b/mwparserfromhell/nodes/text.py index a49930f..1c47c7b 100644 --- a/mwparserfromhell/nodes/text.py +++ b/mwparserfromhell/nodes/text.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -32,7 +32,7 @@ class Text(Node): def __init__(self, value): super(Text, self).__init__() - self._value = value + self.value = value def __unicode__(self): return self.value diff --git a/mwparserfromhell/nodes/wikilink.py b/mwparserfromhell/nodes/wikilink.py index 8f4bf7d..265a100 100644 --- a/mwparserfromhell/nodes/wikilink.py +++ b/mwparserfromhell/nodes/wikilink.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -33,8 +33,8 @@ class Wikilink(Node): def __init__(self, title, text=None): super(Wikilink, self).__init__() - self._title = title - self._text = text + self.title = title + self.text = text def __unicode__(self): if self.text is not None: diff --git a/mwparserfromhell/parser/builder.py b/mwparserfromhell/parser/builder.py index c86a923..f1b9689 100644 --- a/mwparserfromhell/parser/builder.py +++ b/mwparserfromhell/parser/builder.py @@ -48,7 +48,7 @@ def _add_handler(token_type): class Builder(object): """Builds a tree of nodes out of a sequence of tokens. - To use, pass a list of :class:`.Token`\ s to the :meth:`build` method. The + To use, pass a list of :class:`.Token`\\ s to the :meth:`build` method. The list will be exhausted as it is parsed and a :class:`.Wikicode` object containing the node tree will be returned. """ @@ -237,8 +237,7 @@ class Builder(object): else: name, value = self._pop(), None return Attribute(name, value, quotes, start.pad_first, - start.pad_before_eq, start.pad_after_eq, - check_quotes=False) + start.pad_before_eq, start.pad_after_eq) else: self._write(self._handle_token(token)) raise ParserError("_handle_attribute() missed a close token") diff --git a/mwparserfromhell/utils.py b/mwparserfromhell/utils.py index 7387420..d30a2da 100644 --- a/mwparserfromhell/utils.py +++ b/mwparserfromhell/utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -70,5 +70,5 @@ def parse_anything(value, context=0, skip_style_tags=False): nodelist += parse_anything(item, context, skip_style_tags).nodes return Wikicode(nodelist) except TypeError: - error = "Needs string, Node, Wikicode, int, None, or iterable of these, but got {0}: {1}" + error = "Needs string, Node, Wikicode, file, int, None, or iterable of these, but got {0}: {1}" raise ValueError(error.format(type(value).__name__, value)) diff --git a/tests/_test_tree_equality.py b/tests/_test_tree_equality.py index 3c9aa0e..aba54d1 100644 --- a/tests/_test_tree_equality.py +++ b/tests/_test_tree_equality.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -67,7 +67,7 @@ class TreeEqualityTestCase(TestCase): def assertCommentNodeEqual(self, expected, actual): """Assert that two Comment nodes have the same data.""" - self.assertWikicodeEqual(expected.contents, actual.contents) + self.assertEqual(expected.contents, actual.contents) def assertHeadingNodeEqual(self, expected, actual): """Assert that two Heading nodes have the same data.""" diff --git a/tests/test_attribute.py b/tests/test_attribute.py index a36f59f..e9f2528 100644 --- a/tests/test_attribute.py +++ b/tests/test_attribute.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/tests/test_builder.py b/tests/test_builder.py index 67e0043..7343077 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2012-2016 Ben Kurtovic +# Copyright (C) 2012-2019 Ben Kurtovic # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -232,11 +232,11 @@ class TestBuilder(TreeEqualityTestCase): tests = [ ([tokens.CommentStart(), tokens.Text(text="foobar"), tokens.CommentEnd()], - wrap([Comment(wraptext("foobar"))])), + wrap([Comment("foobar")])), ([tokens.CommentStart(), tokens.Text(text="spam"), tokens.Text(text="eggs"), tokens.CommentEnd()], - wrap([Comment(wraptext("spam", "eggs"))])), + wrap([Comment("spameggs")])), ] for test, valid in tests: self.assertWikicodeEqual(valid, self.builder.build(test)) @@ -412,7 +412,7 @@ class TestBuilder(TreeEqualityTestCase): wraptext("c"), params=[Parameter(wraptext("1"), wrap([Wikilink( wraptext("d")), Argument(wraptext("e"))]), showkey=False)])]), showkey=False)]), Wikilink(wraptext("f"), wrap([Argument(wraptext( - "g")), Comment(wraptext("h"))])), Template(wraptext("i"), params=[ + "g")), Comment("h")])), Template(wraptext("i"), params=[ Parameter(wraptext("j"), wrap([HTMLEntity("nbsp", named=True)]))])]) self.assertWikicodeEqual(valid, self.builder.build(test))