Jonathan Eunice committed f26236e

indent option added; docs and tests improved

Comments (0)

Files changed (4)

     **A:** Yes! ZOMG, yes!
-``say`` provides straightforward string formatting with a DRY, Pythonic
-templating approach. It piggybacks the ``format()`` method, using its 
-formatting syntax (and underlying engine).
+``say`` provides:
+ *  Straightforward string formatting with a DRY, Pythonic
+    templates that piggyback the built in ``format()`` method,  
+    formatting syntax, and well-proven underlying engine.
+ *  A single output mechanism compatible with both Python 2.x and Python 3.x.
+ *  Indentation (to help stucture output)
+ *  Convenience printing functions for horizontal rules (lines), titles, and
+    vertical whitespace.
     print "Nums has {} items: {}".format(len(nums), nums)
 (NB in Python 2.6
-one must number each of the ``{}`` placeholders
-in order to avoid a ``ValueError: zero length field name in format`` error.
-E.g. ``"Nums has {0} items: {1}"``)
+one must number each of the ``{}`` placeholders--e.g. ``"Nums has {0} items: {1}"``--
+in order to avoid a ``ValueError: zero length field name in format`` error. Python 2.7
+and later assume the placeholders are sequential.)
 The more items that are being printed, and the complicated the ``format``
 invocation, the more valuable having it stated in-line becomes. Note that full
     say.setfiles(stdout, "report.txt")
     say(...)   # now prints to both stdout and report.txt
-This has the advantage of allowing you to capture program output without changing
+This has the advantage of allowing you to both capture and see
+program output, without changing
 any code. You can also define your own targeted ``Say`` instances::
     from say import say, Say, stderr
 doesn't print. (The
 ``C`` analogy: ``say`` **:** ``fmt`` **::** ``printf`` **:** ``sprintf``.)
+Indentation is a common way to display data hierarchically. ``say`` will
+help you manage it. For example::
+    say('TITLE')
+    for item in items:
+        say(item, indent=1)
+will indent the items by one indentation level (by default, each indent
+level is four spaces, but
+you can change that with the ``indent_str`` option). 
+If you want to change the default indentation level::
+    say.set(indent=1)      # to an absolute level
+    say.set(indent='+1')   # strings => set relative to current level
+    ...
+    say.set(indent=0)      # to get back to the default, no indent
+Or you can use a ``with`` construct::
+    with say.settings(indent='+1'):
+        say(...)
+        # anything say() emits here will be auto-indented +1 levels
+While it's easy enough for any ``print`` statement or function to have a few
+space characters added to its format string, it's easy to mistakenly type
+too many or too few spaces, or to forget to type them in some format strings.
+And if you're indenting strings that
+themselves may contain multiple lines, the simple ``print`` approach breaks
+because won't take multi-line strings into account.
+``say``, however, simply handles the indent level, and it properly
+handles the multi-line string case. Their
+subsequent lines will be just as nicely and correctly indented as the
+first one--something not otherwise easily accomplished without adding 
+gunky, complexifying string manipulation code to every place in your program
+that prints strings.
+This starts to illustrate the "do the right thing" philosophy behind ``say``.
+So many languages' printing and formatting functions a restricted to 
+"outputting values" at a low level. They may format basic data types, but
+they don't provide straightforward
+ways to do neat text transformations like indentation that let programmers
+rapidly provide correct, highly-formatted ouput. Over time, ``say`` will provide
+higher-level formatting options. For now: indentation.
 ``say()`` and 
 ``fmt()`` try to work with Unicode strings, for example providing them as
 return values. But character encodings remain a fractious and often exasperating
-part of IT. When writing to files, ``say`` handles this with an 
+part of IT. When writing formatted strings, ``say`` handles this by encoding
+into ``utf-8``.
-If you are using Python 2.7
-with strings containing ``utf-8`` rather than Unicode characters, ``say`` will
-not be greatly happy--but basically in the same places that ``format()`` is
-already not happy.
+If you are using strings containing ``utf-8`` rather than Unicode characters, ``say`` 
+may complain. But it complains in the same places the built-in ``format()`` does,
+so no harm done. (Python 3 doesn't generally allow ``utf-8`` in strings, so it's
+cleaner on this front.)
-When writing to files under Python 2.7, ``say`` writes using an encoding
-(by default, ``utf-8``). But you can get creative::
+You can get creative with the encoding::
     say('I am a truck!', encoding='base64')  # SSBhbSBhIHRydWNrIQo=
 Knock yourself out with `all the exciting opportunites <>`_!
 If you really want the formatted text returned just as it is written to files,
-use the ``encoded`` option. Set to ``True`` it returns text in the output
-encoding. Or set to anything else, that becomes the return encoding.
+use the ``encoded`` option. Set to ``True`` and it returns text in the output
+encoding. Or set to an actual encoding name, and that will be the return encoding.
 ``say()`` returns the formatted text with one small tweak: it removes the final
 newline if a newline is the very last character. Though odd, this is exactly
 Python 3
-Say works the same way in both Python 2 and Python 3. This can simplify 
+Say works virtually the same way in Python 2 and Python 3. This can simplify 
 software that should work across the versions, without all the ``from __future__
 import print_function`` hassle.
 ``say`` attempts to mask some of the quirky compexities of the 2-to-3 divide,
-such as string encodings and codec use.
+such as string encodings and codec use. 
     options = Options(  
+        indent=0,           # indent level (if set to None, indentation is turned off)
+        indent_str='    ',  # indent string for each level
         encoding=None if six.PY3 else 'utf-8',
                             # character encoding for output (needed on Python 2, not 3)
         encoded=None,       # character encoding to return
         retvalue=False,     # should return the value of the formatted string?
         _callframe=None,    # frome from which the caller was calling
-    options.files = DEFAULT_FILES
+    options.files = DEFAULT_FILES # to get around a bug in stuf
+    options.magic(
+        indent = lambda v, cur: cur.indent + int(v) if isinstance(v, str) else v
+    )
     def __init__(self, **kwargs):
         formatted = [ _sprintf(arg, opts._callframe) if is_string(arg) else str(arg)
                       for arg in args ]
-        return self._output_str(opts.sep.join(formatted), opts)
+        return self._output(opts.sep.join(formatted), opts)
-    def _output_str(self, outstr, opts):
+    def _output(self, data, opts):
-        Do the actual formatting and outputting work. 
+        Do the actual formatting and outputting work. ``data`` may be either a
+        list of lines, or a composed string.
+        if opts.indent:
+            indent_str = opts.indent_str * opts.indent
+            datalines = data if isinstance(data, list) else data.splitlines()
+            outstr = '\n'.join([ ''.join([ indent_str, line ]) for line in datalines ])
+        else:
+            outstr = '\n'.join(data) if isinstance(data, list) else data
+        # by end of indenting, dealing with string only
         # prepare and emit output
         if opts.end is not None:
             retencoded = encoded(outstr, retencoding)
             rettrimmed = retencoded[:-1] if retencoded.endswith('\n') else retencoded
             return rettrimmed
-    def _output(self, lines, opts):
-        """
-        Alternate output function that accepts list of lines, not composed strings.
-        More important and logical in the future. Stay tuned.
-        """
-        return self._output_str("\n".join(lines), opts)
     def _write(self, s, opts):
         Write s to all associated file objects. 
 say = Say()
-# patch for stuf() error
-if not isinstance(Say.options.files, list):
-    Say.options.files = [ Say.options.files ]
 fmt = say.clone(encoding=None, retvalue=True, silent=True)
-    version=verno("0.311"),
+    version=verno("0.401"),
     author='Jonathan Eunice',
-    description='Simple formatted printing with templates. E.g.: say("Hello, {whoever}!")',
+    description='Simple printing with templates. E.g.: say("Hello, {whoever}!", indent=1)',
     r1 = say("this is output")
     r2 = say("this is output", silent=True)
     assert r1 == r2
+def test_indent():
+    setup()
+    assert say('no indent') == 'no indent'
+    assert say('no indent', indent=0) == 'no indent'
+    assert say('one indent', indent='1')  == '    one indent'
+    assert say('one indent', indent='+1') == '    one indent'
+    assert say('two indent', indent=2) == '        two indent'
+    say.set(indent=1)
+    assert say('auto one indent') == '    auto one indent'
+    assert say('one plus one indent', indent='+1') == '        one plus one indent'
+    assert say('subtract indent', indent='-1') == 'subtract indent'
+    assert say('force no indent', indent=0) == 'force no indent'
+    say.set(indent=0)
+    assert say('no indent again') == 'no indent again'
+def test_indent_special():
+    setup()
+    say.set(indent_str='>>> ')
+    assert say('something') == 'something'
+    assert say('else', indent=1) == '>>> else'
+    assert say('again', indent=2) == '>>> >>> again'
+    say.set(indent_str='| ')
+    assert say("some text") == 'some text'
+    assert say("other", indent="+1") == '| other'
+def test_indent_multiline():
+    setup()
+    assert say('and off\nwe go', indent='+1') == '    and off\n    we go'
+def test_with_indent():
+    setup()
+    with say.settings(indent='+1'):
+        assert say("I am indented!") == "    I am indented!"
+        with say.settings(indent='+1'):
+            assert say("xyz") == "        xyz"
+        assert say('one back') == '    one back'
+    assert say('back again') == 'back again'
+def test_example_3():
+    setup()
+    items = '1 2 3'.split()
+    assert say('TITLE') == 'TITLE'
+    for item in items:
+        assert say(item, indent=1) == '    ' + str(item)
 globalvar = 99