Repository: chimpler/pyhocon

Issue:
[bug] pyhocon conflicts with others package that uses pyparsing
in file config_parser.py line 278, there is a code:
 
    ParserElement.setDefaultWhitespaceChars(' \t')

It probably causes other packages failed if it depends on pyparsing.
for exampel:

    from pyhocon import ConfigFactory
    import pydot
    out = ConfigFactory.parse_file("my.conf")
    digraph = pydot.graph_from_dot_file(dot_file)[0] # failed

Comments:

Patch:
diff --git a/pyhocon/config_parser.py b/pyhocon/config_parser.py
index 4326980..cc18eef 100644
--- a/pyhocon/config_parser.py
+++ b/pyhocon/config_parser.py
@@ -275,80 +275,86 @@ class ConfigParser(object):
 
             return ConfigInclude(obj if isinstance(obj, list) else obj.items())
 
-        ParserElement.setDefaultWhitespaceChars(' \t')
-
-        assign_expr = Forward()
-        true_expr = Keyword("true", caseless=True).setParseAction(replaceWith(True))
-        false_expr = Keyword("false", caseless=True).setParseAction(replaceWith(False))
-        null_expr = Keyword("null", caseless=True).setParseAction(replaceWith(NoneValue()))
-        key = QuotedString('"', escChar='\\', unquoteResults=False) | Word(alphanums + alphas8bit + '._- /')
-
-        eol = Word('\n\r').suppress()
-        eol_comma = Word('\n\r,').suppress()
-        comment = (Literal('#') | Literal('//')) - SkipTo(eol | StringEnd())
-        comment_eol = Suppress(Optional(eol_comma) + comment)
-        comment_no_comma_eol = (comment | eol).suppress()
-        number_expr = Regex(r'[+-]?(\d*\.\d+|\d+(\.\d+)?)([eE][+\-]?\d+)?(?=$|[ \t]*([\$\}\],#\n\r]|//))',
-                            re.DOTALL).setParseAction(convert_number)
-
-        # multi line string using """
-        # Using fix described in http://pyparsing.wikispaces.com/share/view/3778969
-        multiline_string = Regex('""".*?"*"""', re.DOTALL | re.UNICODE).setParseAction(parse_multi_string)
-        # single quoted line string
-        quoted_string = Regex(r'"(?:[^"\\\n]|\\.)*"[ \t]*', re.UNICODE).setParseAction(create_quoted_string)
-        # unquoted string that takes the rest of the line until an optional comment
-        # we support .properties multiline support which is like this:
-        # line1  \
-        # line2 \
-        # so a backslash precedes the \n
-        unquoted_string = Regex(r'(?:[^^`+?!@*&"\[\{\s\]\}#,=\$\\]|\\.)+[ \t]*', re.UNICODE).setParseAction(unescape_string)
-        substitution_expr = Regex(r'[ \t]*\$\{[^\}]+\}[ \t]*').setParseAction(create_substitution)
-        string_expr = multiline_string | quoted_string | unquoted_string
-
-        value_expr = number_expr | true_expr | false_expr | null_expr | string_expr
-
-        include_content = (quoted_string | ((Keyword('url') | Keyword('file')) - Literal('(').suppress() - quoted_string - Literal(')').suppress()))
-        include_expr = (
-            Keyword("include", caseless=True).suppress() + (
-                include_content | (
-                    Keyword("required") - Literal('(').suppress() - include_content - Literal(')').suppress()
+        @contextlib.contextmanager
+        def set_default_white_spaces():
+            default = ParserElement.DEFAULT_WHITE_CHARS
+            ParserElement.setDefaultWhitespaceChars(' \t')
+            yield
+            ParserElement.setDefaultWhitespaceChars(default)
+
+        with set_default_white_spaces():
+            assign_expr = Forward()
+            true_expr = Keyword("true", caseless=True).setParseAction(replaceWith(True))
+            false_expr = Keyword("false", caseless=True).setParseAction(replaceWith(False))
+            null_expr = Keyword("null", caseless=True).setParseAction(replaceWith(NoneValue()))
+            key = QuotedString('"', escChar='\\', unquoteResults=False) | Word(alphanums + alphas8bit + '._- /')
+
+            eol = Word('\n\r').suppress()
+            eol_comma = Word('\n\r,').suppress()
+            comment = (Literal('#') | Literal('//')) - SkipTo(eol | StringEnd())
+            comment_eol = Suppress(Optional(eol_comma) + comment)
+            comment_no_comma_eol = (comment | eol).suppress()
+            number_expr = Regex(r'[+-]?(\d*\.\d+|\d+(\.\d+)?)([eE][+\-]?\d+)?(?=$|[ \t]*([\$\}\],#\n\r]|//))',
+                                re.DOTALL).setParseAction(convert_number)
+
+            # multi line string using """
+            # Using fix described in http://pyparsing.wikispaces.com/share/view/3778969
+            multiline_string = Regex('""".*?"*"""', re.DOTALL | re.UNICODE).setParseAction(parse_multi_string)
+            # single quoted line string
+            quoted_string = Regex(r'"(?:[^"\\\n]|\\.)*"[ \t]*', re.UNICODE).setParseAction(create_quoted_string)
+            # unquoted string that takes the rest of the line until an optional comment
+            # we support .properties multiline support which is like this:
+            # line1  \
+            # line2 \
+            # so a backslash precedes the \n
+            unquoted_string = Regex(r'(?:[^^`+?!@*&"\[\{\s\]\}#,=\$\\]|\\.)+[ \t]*', re.UNICODE).setParseAction(unescape_string)
+            substitution_expr = Regex(r'[ \t]*\$\{[^\}]+\}[ \t]*').setParseAction(create_substitution)
+            string_expr = multiline_string | quoted_string | unquoted_string
+
+            value_expr = number_expr | true_expr | false_expr | null_expr | string_expr
+
+            include_content = (quoted_string | ((Keyword('url') | Keyword('file')) - Literal('(').suppress() - quoted_string - Literal(')').suppress()))
+            include_expr = (
+                Keyword("include", caseless=True).suppress() + (
+                    include_content | (
+                        Keyword("required") - Literal('(').suppress() - include_content - Literal(')').suppress()
+                    )
                 )
+            ).setParseAction(include_config)
+
+            root_dict_expr = Forward()
+            dict_expr = Forward()
+            list_expr = Forward()
+            multi_value_expr = ZeroOrMore(comment_eol | include_expr | substitution_expr | dict_expr | list_expr | value_expr | (Literal(
+                '\\') - eol).suppress())
+            # for a dictionary : or = is optional
+            # last zeroOrMore is because we can have t = {a:4} {b: 6} {c: 7} which is dictionary concatenation
+            inside_dict_expr = ConfigTreeParser(ZeroOrMore(comment_eol | include_expr | assign_expr | eol_comma))
+            inside_root_dict_expr = ConfigTreeParser(ZeroOrMore(comment_eol | include_expr | assign_expr | eol_comma), root=True)
+            dict_expr << Suppress('{') - inside_dict_expr - Suppress('}')
+            root_dict_expr << Suppress('{') - inside_root_dict_expr - Suppress('}')
+            list_entry = ConcatenatedValueParser(multi_value_expr)
+            list_expr << Suppress('[') - ListParser(list_entry - ZeroOrMore(eol_comma - list_entry)) - Suppress(']')
+
+            # special case when we have a value assignment where the string can potentially be the remainder of the line
+            assign_expr << Group(
+                key - ZeroOrMore(comment_no_comma_eol) - (dict_expr | (Literal('=') | Literal(':') | Literal('+=')) - ZeroOrMore(
+                    comment_no_comma_eol) - ConcatenatedValueParser(multi_value_expr))
             )
-        ).setParseAction(include_config)
-
-        root_dict_expr = Forward()
-        dict_expr = Forward()
-        list_expr = Forward()
-        multi_value_expr = ZeroOrMore(comment_eol | include_expr | substitution_expr | dict_expr | list_expr | value_expr | (Literal(
-            '\\') - eol).suppress())
-        # for a dictionary : or = is optional
-        # last zeroOrMore is because we can have t = {a:4} {b: 6} {c: 7} which is dictionary concatenation
-        inside_dict_expr = ConfigTreeParser(ZeroOrMore(comment_eol | include_expr | assign_expr | eol_comma))
-        inside_root_dict_expr = ConfigTreeParser(ZeroOrMore(comment_eol | include_expr | assign_expr | eol_comma), root=True)
-        dict_expr << Suppress('{') - inside_dict_expr - Suppress('}')
-        root_dict_expr << Suppress('{') - inside_root_dict_expr - Suppress('}')
-        list_entry = ConcatenatedValueParser(multi_value_expr)
-        list_expr << Suppress('[') - ListParser(list_entry - ZeroOrMore(eol_comma - list_entry)) - Suppress(']')
-
-        # special case when we have a value assignment where the string can potentially be the remainder of the line
-        assign_expr << Group(
-            key - ZeroOrMore(comment_no_comma_eol) - (dict_expr | (Literal('=') | Literal(':') | Literal('+=')) - ZeroOrMore(
-                comment_no_comma_eol) - ConcatenatedValueParser(multi_value_expr))
-        )
-
-        # the file can be { ... } where {} can be omitted or []
-        config_expr = ZeroOrMore(comment_eol | eol) + (list_expr | root_dict_expr | inside_root_dict_expr) + ZeroOrMore(
-            comment_eol | eol_comma)
-        config = config_expr.parseString(content, parseAll=True)[0]
-
-        if resolve:
-            allow_unresolved = resolve and unresolved_value is not DEFAULT_SUBSTITUTION and unresolved_value is not MANDATORY_SUBSTITUTION
-            has_unresolved = cls.resolve_substitutions(config, allow_unresolved)
-            if has_unresolved and unresolved_value is MANDATORY_SUBSTITUTION:
-                raise ConfigSubstitutionException('resolve cannot be set to True and unresolved_value to MANDATORY_SUBSTITUTION')
-
-        if unresolved_value is not NO_SUBSTITUTION and unresolved_value is not DEFAULT_SUBSTITUTION:
-            cls.unresolve_substitutions_to_value(config, unresolved_value)
+
+            # the file can be { ... } where {} can be omitted or []
+            config_expr = ZeroOrMore(comment_eol | eol) + (list_expr | root_dict_expr | inside_root_dict_expr) + ZeroOrMore(
+                comment_eol | eol_comma)
+            config = config_expr.parseString(content, parseAll=True)[0]
+
+            if resolve:
+                allow_unresolved = resolve and unresolved_value is not DEFAULT_SUBSTITUTION and unresolved_value is not MANDATORY_SUBSTITUTION
+                has_unresolved = cls.resolve_substitutions(config, allow_unresolved)
+                if has_unresolved and unresolved_value is MANDATORY_SUBSTITUTION:
+                    raise ConfigSubstitutionException('resolve cannot be set to True and unresolved_value to MANDATORY_SUBSTITUTION')
+
+            if unresolved_value is not NO_SUBSTITUTION and unresolved_value is not DEFAULT_SUBSTITUTION:
+                cls.unresolve_substitutions_to_value(config, unresolved_value)
         return config
 
     @classmethod
diff --git a/pyhocon/config_tree.py b/pyhocon/config_tree.py
index 1492793..c39a977 100644
--- a/pyhocon/config_tree.py
+++ b/pyhocon/config_tree.py
@@ -1,7 +1,6 @@
 from collections import OrderedDict
 from pyparsing import lineno
 from pyparsing import col
-
 try:
     basestring
 except NameError:  # pragma: no cover
@@ -364,6 +363,14 @@ class ConfigTree(OrderedDict):
             raise KeyError(item)
         return val
 
+    try:
+        from collections import _OrderedDictItemsView
+    except ImportError:  # pragma: nocover
+        pass
+    else:
+        def items(self):  # pragma: nocover
+            return self._OrderedDictItemsView(self)
+
     def __getattr__(self, item):
         val = self.get(item, NonExistentKey)
         if val is NonExistentKey:


Test patch:
diff --git a/tests/test_config_tree.py b/tests/test_config_tree.py
index fbdee7b..1435cff 100644
--- a/tests/test_config_tree.py
+++ b/tests/test_config_tree.py
@@ -1,6 +1,6 @@
 import pytest
 from collections import OrderedDict
-from pyhocon.config_tree import ConfigTree
+from pyhocon.config_tree import ConfigTree, NoneValue
 from pyhocon.exceptions import (
     ConfigMissingException, ConfigWrongTypeException, ConfigException)
 
@@ -70,6 +70,11 @@ class TestConfigTree(object):
         config_tree.put("a.b.c", None)
         assert config_tree.get("a.b.c") is None
 
+    def test_config_tree_null_items(self):
+        config_tree = ConfigTree()
+        config_tree.put("a", NoneValue())
+        assert list(config_tree.items()) == [("a", None)]
+
     def test_getters(self):
         config_tree = ConfigTree()
         config_tree.put("int", 5)