Bug 1296503 - Add an indented_repr function to mozbuild.util. r?ted draft
authorMike Hommey <mh+mozilla@glandium.org>
Thu, 18 Aug 2016 18:18:04 +0900
changeset 404296 82c715bc29fc61dfe9a10ca97420301e1e95a697
parent 404295 9ce516ab44fefe64f5e10cdf87cd57b1d3083834
child 404297 2131623920e2aa38a8591696b881afe29aa0adaf
push id27158
push userbmo:mh+mozilla@glandium.org
push dateTue, 23 Aug 2016 05:25:26 +0000
reviewersted
bugs1296503
milestone51.0a1
Bug 1296503 - Add an indented_repr function to mozbuild.util. r?ted
python/mozbuild/mozbuild/test/test_util.py
python/mozbuild/mozbuild/util.py
--- a/python/mozbuild/mozbuild/test/test_util.py
+++ b/python/mozbuild/mozbuild/test/test_util.py
@@ -1,34 +1,37 @@
+# coding: utf-8
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import unicode_literals
 
 import itertools
 import hashlib
 import os
 import unittest
 import shutil
 import string
 import sys
 import tempfile
+import textwrap
 
 from mozfile.mozfile import NamedTemporaryFile
 from mozunit import (
     main,
     MockedOpen,
 )
 
 from mozbuild.util import (
     expand_variables,
     FileAvoidWrite,
     group_unified_files,
     hash_file,
+    indented_repr,
     memoize,
     memoized_property,
     pair,
     resolve_target_to_make,
     MozbuildDeletionError,
     HierarchicalStringList,
     EnumString,
     EnumStringComparisonError,
@@ -882,10 +885,40 @@ class TestEnumString(unittest.TestCase):
             self.assertNotEquals(type, 'foo')
 
         with self.assertRaises(EnumStringComparisonError):
             self.assertIn(type, ('foo', 'gcc'))
 
         with self.assertRaises(ValueError):
             type = CompilerType('foo')
 
+
+class TestIndentedRepr(unittest.TestCase):
+    def test_indented_repr(self):
+        data = textwrap.dedent(r'''
+        {
+            'a': 1,
+            'b': b'abc',
+            b'c': 'xyz',
+            'd': False,
+            'e': {
+                'a': 1,
+                'b': b'2',
+                'c': '3',
+            },
+            'f': [
+                1,
+                b'2',
+                '3',
+            ],
+            'pile_of_bytes': b'\xf0\x9f\x92\xa9',
+            'pile_of_poo': '💩',
+            'special_chars': '\\\'"\x08\n\t',
+            'with_accents': 'éàñ',
+        }''').lstrip()
+
+        obj = eval(data)
+
+        self.assertEqual(indented_repr(obj), data)
+
+
 if __name__ == '__main__':
     main()
--- a/python/mozbuild/mozbuild/util.py
+++ b/python/mozbuild/mozbuild/util.py
@@ -1171,8 +1171,76 @@ class EnumString(unicode):
     def __ne__(self, other):
         return not (self == other)
 
     @staticmethod
     def subclass(*possible_values):
         class EnumStringSubclass(EnumString):
             POSSIBLE_VALUES = possible_values
         return EnumStringSubclass
+
+
+def _escape_char(c):
+    # str.encode('unicode_espace') doesn't escape quotes, presumably because
+    # quoting could be done with either ' or ".
+    if c == "'":
+        return "\\'"
+    return unicode(c.encode('unicode_escape'))
+
+# Mapping table between raw characters below \x80 and their escaped
+# counterpart, when they differ
+_INDENTED_REPR_TABLE = {
+    c: e
+    for c, e in map(lambda x: (x, _escape_char(x)),
+                    map(unichr, range(128)))
+    if c != e
+}
+# Regexp matching all characters to escape.
+_INDENTED_REPR_RE = re.compile(
+    '([' + ''.join(_INDENTED_REPR_TABLE.values()) + ']+)')
+
+
+def indented_repr(o, indent=4):
+    '''Similar to repr(), but returns an indented representation of the object
+
+    One notable difference with repr is that the returned representation
+    assumes `from __future__ import unicode_literals`.
+    '''
+    one_indent = ' ' * indent
+    def recurse_indented_repr(o, level):
+        if isinstance(o, dict):
+            yield '{\n'
+            for k, v in sorted(o.items()):
+                yield one_indent * (level + 1)
+                for d in recurse_indented_repr(k, level + 1):
+                    yield d
+                yield ': '
+                for d in recurse_indented_repr(v, level + 1):
+                    yield d
+                yield ',\n'
+            yield one_indent * level
+            yield '}'
+        elif isinstance(o, bytes):
+            yield 'b'
+            yield repr(o)
+        elif isinstance(o, unicode):
+            yield "'"
+            # We want a readable string (non escaped unicode), but some
+            # special characters need escaping (e.g. \n, \t, etc.)
+            for i, s in enumerate(_INDENTED_REPR_RE.split(o)):
+                if i % 2:
+                    for c in s:
+                        yield _INDENTED_REPR_TABLE[c]
+                else:
+                    yield s
+            yield "'"
+        elif hasattr(o, '__iter__'):
+            yield '[\n'
+            for i in o:
+                yield one_indent * (level + 1)
+                for d in recurse_indented_repr(i, level + 1):
+                    yield d
+                yield ',\n'
+            yield one_indent * level
+            yield ']'
+        else:
+            yield repr(o)
+    return ''.join(recurse_indented_repr(o, 0))