Commits

Łukasz Langa  committed a9746d1

List printer pretty much done.

  • Participants
  • Parent commits b9cb3e1

Comments (0)

Files changed (1)

File nattyprint.py

 import re
 import sys
 
-formatters = []
-
-def _indent(text, indent=0, first_line=True):
-    lines = text.split('\n')
-    result = "\n".join([" "*indent + line for line in lines])
-    if not first_line:
-        result = result[indent:]
-    return result
-
 
 class Formatter:
     @classmethod
                 isinstance(other.__repr__, Callable))
 
     @classmethod
-    def __pprint__(cls, instance, width=0, height=0):
-        result = repr(instance)
-        lines = result.split('\n')
-        result_width = max([len(line) for line in lines])
-        result_height = len(lines)
-        return result, result_width, result_height
-
-
-class StringFormatter(str):
-    @classmethod
-    def __pprint__(cls, instance, width=0, height=0):
-        result = repr(instance)
-        result_width = len(result)
-        result_height = 1
-        if width and width < len(result):
-            m = re.match("^([a-z]{0,2}['\"]).*(['\"])$", result)
-            if m:
-                buffer = io.StringIO()
-                prefix, suffix = m.group(1), m.group(2)
-                result = result[len(prefix):-len(suffix)]
-                scaffolding_width = len(prefix) + len(suffix) + 2 # brackets 
-                real_width = max(width - scaffolding_width, 1)
-                # FIXME: naive chunking
-                result_height = 0
-                chunks = round(len(result)/real_width+0.5)
-                for i in range(chunks):
-                    if i == 0:
-                        buffer.write("(")
-                    else:
-                        buffer.write("\n ")
-                    chunk = result[real_width*i:real_width*(i+1)]
-                    result_width = len(chunk) + scaffolding_width
-                    result_height += 1
-                    buffer.write(prefix)
-                    buffer.write(chunk)
-                    buffer.write(suffix)
-                buffer.write(")")
-                result = buffer.getvalue()
-        return result, result_width, result_height
-
-
-class ListFormatter(list):
-    PPRINT_WIDE = True # multiple values on one line
+    def _pprint(cls, instance, width, height, buffer):
+        buffer.write(repr(instance))
 
     @classmethod
-    def __pprint__(cls, instance, width=0, height=0):
-        buffer = io.StringIO()
+    def __pprint__(cls, instance, width=None, height=None):
+        """Returns (result, width_of_the_last_line, number_of_lines)."""
+        if width is None:
+            width = sys.maxsize
+        if height is None:
+            height = sys.maxsize
+        if width <= 0:
+            import pdb; pdb.set_trace()
+        assert width > 0
+        assert height > 0
+        buffer = _SizeMeter()
+        cls._pprint(instance, width, height, buffer)
+        result = buffer.getvalue()
+        assert result is not None
+        assert buffer.height > 0
+        assert buffer.width >= 0
+        return result, buffer.width, buffer.height
+
+
+class StringFormatter(Formatter):
+    @classmethod
+    def __accepts__(cls, other):
+        return other.__class__ is str
+
+    @classmethod
+    def _pprint(cls, instance, width, height, buffer):
+        result = repr(instance)
+        if width is not None and width < len(result):
+            match = re.match("^([a-z]{0,2}['\"]).*(['\"])$", result)
+            assert match is not None
+            prefix, suffix = match.group(1), match.group(2)
+            result = result[len(prefix):-len(suffix)]
+            scaffolding_width = len(prefix) + len(suffix) + 2 # brackets 
+            real_width = max(width - scaffolding_width, 1)
+            # FIXME: naive chunking
+            chunks = round(len(result)/real_width+0.5)
+            for i in range(chunks):
+                if i == 0:
+                    buffer.write("(")
+                else:
+                    buffer.write("\n ")
+                chunk = result[real_width*i:real_width*(i+1)]
+                buffer.write(prefix)
+                buffer.write(chunk)
+                buffer.write(suffix)
+            buffer.write(")")
+        else:
+            buffer.write(result)
+
+
+class ListFormatter(Formatter):
+    # Whether multiple values should be printed on one line
+    MULTIPLE_VALUES = True
+
+    @classmethod
+    def __accepts__(cls, other):
+        return other.__class__ is list
+
+    @classmethod
+    def _pprint(cls, instance, width, height, buffer):
+        def enough_space(elem_width=1, elem_height=1):
+            result = (width - buffer.width - elem_width - 1 > 0 or
+                      elem_height > 1)
+            return cls.MULTIPLE_VALUES and result
+
         buffer.write("[")
-        current_width = 1
+        available_width = sys.maxsize
+        first_elem = True
         for elem in instance:
+            got_newline = False
+            if not first_elem:
+                if enough_space():
+                    buffer.write(", ")
+                else:
+                    got_newline = True
+                    buffer.write(",\n")
+
             fmt_elem = select_formatter(elem)
-            available_width = width - current_width - 1 if width else 0
-            pp_elem, pp_width, pp_height = fmt_elem.__print__(elem,
-                                           width=available_width)
-            if width and pp_width + current_width + 1 > width:
-                ?
-        return buffer.getvalue(), result_width, result_height
+            printed = fmt_elem.__pprint__(elem)
+            elem_repr, elem_width, elem_height = printed
 
+            if not enough_space(elem_width, elem_height):
+                if not first_elem and not got_newline:
+                    got_newline = True
+                    buffer.write("\n")
+                available_width = width - buffer.width - 1
+                printed = fmt_elem.__pprint__(elem, width=available_width)
+                elem_repr, elem_width, elem_height = printed
 
-# TODO: teraz trzeba sprawdzić jak decydować kiedy dalej wcinać, a kiedy od
-# początku jechać (od nowej linii. Wydaje mi się, że dalej wcinać tylko
-# z jednolinijkowymi.
+            r = _indent(elem_repr, 1, omit_first_line=not got_newline)
 
-builtin_formatters = {
+            buffer.write(r)
+            first_elem = False
+        buffer.write("]")
+
+formatters = []
+
+_builtin_formatters = {
     dict: Formatter,
     list: ListFormatter,
     str: StringFormatter,
 }
 
+def pprint(object, stream=sys.stdout, indent=None, width=80, depth=None):
+    """Utility function for printing out any object in a natty way. API mimics
+    pprint.pprint."""
+    if indent is not None:
+        raise NotImplementedError("Not implemented: changing indentation.")
+    if depth is not None:
+        raise NotImplementedError("Not implemented: changing depth.")
+    formatter = select_formatter(object)
+    stream.write(formatter.__pprint__(object, width=width)[0])
+    stream.write('\n')
 
 def select_formatter(object):
+    """Searches for applicable formatter for the specified 'object'. First
+    in 'formatters', then in '_builtin_formatters'. In each case when an exact
+    match is not found, matches are searched for ancestor classes in MRO order.
+    """
+
     mro = object.__mro__ if hasattr(object, '__mro__') else [object.__class__]
     for cls in mro:
         for formatter in formatters:
                                     formatter.__accepts__(cls)):
                 return formatter
     for cls in mro:
-        if cls in builtin_formatters:
-            return builtin_formatters[cls]
+        if cls in _builtin_formatters:
+            return _builtin_formatters[cls]
     return Formatter
 
 
-def pprint(object, target=sys.stdout, width=80):
-    formatter = select_formatter(object)
-    target.write(formatter.__pprint__(object, width=width)[0])
-    target.write('\n')
+class _SizeMeter(io.StringIO):
+    """StringIO storing information on the number of characters on the last
+       line (width) and the number of lines (height)."""
+
+    _line_regex = re.compile(r"\n", re.DOTALL)
+    _last_line = re.compile(r"^.*?([^\n]*)$", re.DOTALL) # beware: does not
+                                                         # count the very last
+                                                         # line if it's empty.
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.width = 0
+        self.height = 0
+
+    def write(self, text, *args, **kwargs):
+        assert self.width >= 0
+        assert self.height >= 0
+        super().write(text, *args, **kwargs)
+        lines = len(self._line_regex.findall(text))
+        if not self.height and text:
+            self.height = 1
+        self.height += lines
+        if lines:
+            self.width = 0
+        if not text.endswith("\n"):
+            match = self._last_line.match(text)
+            assert match is not None
+            self.width += len(match.group(1))
+        assert self.width >= 0
+        assert self.height >= 0
+
+
+def _indent(text, indent=0, omit_first_line=False):
+    lines = text.split('\n')
+    result = "\n".join([" "*indent + line for line in lines])
+    if omit_first_line:
+        result = result[indent:]
+    return result
 
 if __name__ == '__main__':
-    WIDTH = 80
+    WIDTH = 30
+    _width = WIDTH if WIDTH else 80
+    import random
     import pprint as old
-    def both(object):
+    def both(object, only_natty=False):
         pprint(object, width=WIDTH)
-        print("""
+        if not only_natty:
+            print("""
   vs.
+""")
+            old.pprint(object)
+        print("*" * _width)
 
-""")
-        old.pprint(object)
-        print("*" * WIDTH)
-    print("*" * WIDTH)
+    print("*" * _width)
+    a = lambda: ["#" * random.randrange(1, 10) for i in range(random.randrange(10, 20))]
+    b = lambda: [a() for i in range(10)]
+    funkylist = [b() for i in range(5)]
+    both(funkylist, only_natty=True)
+    both([12345] * 20, only_natty=True)
+
+if __name__ != '__main__':
+    from textwrap import dedent
+    test1 = dedent("""
+                   Line 1
+                   Line 2
+                   Line 3
+                   Line 4 is longer than the rest
+                   Line 5""")
+    s = _SizeMeter()
+    for line in test1[1:].split("\n"):
+        if line != "Line 5":
+            line += "\n"
+        s.write(line)
+    assert s.width == 6
+    assert s.height == 5
+
+
     both([])
-    both(list(range(10)))
+    both(list(range(50)))
+    both(funkylist, only_natty=True)
+    both([12345] * 30)
     both("Magda")
     both("""Wielolinijkowy