Commits

Jonathan Eunice committed 86d2076

Nearing completion for 1.1.0

  • Participants
  • Parent commits f9fd3f4

Comments (0)

Files changed (10)

     print("There are {0} things.".format(x))
     print("Nums has {0} items: {1}".format(len(nums), nums))
 
-(And yes, you really do need that ``import`` and the 
+(And yes, you really do need that ``import`` and the
 numerical sequencing of ``{}`` format specs if you want code
 that works correctly from Python 2.6 forward from
 a single code base.)
   * A single output mechanism identical and compatible across Python 2 and
     Python 3.
   * A companion ``fmt()`` object for string formatting.
-  * Higher-order line formatting such as
-    indentation and wrapping built in.
+  * Higher-order line formatting such as line numbering,
+    indentation, and wrapping built in.
   * Convenient methods for common formatting items such as
     titles,
     horizontal separators, and

File docs/CHANGES.rst

 Changes
 =======
 
+1.1.0 (September ??, 2013)
+''''''''''''''''''''''''''
+
+  * Line numbering now an optional way to format output.
+  * Line wrapping is now much more precise. The ``wrap`` parameter now
+    specifies the line length desired, including however many characters
+    are consumed by prefix, suffix, and indentation.
+  * Vertical spacing is regularized and much better tested. The ``vsep``
+    option, previously available only on a few methods, is now available
+    everywhere. ``vsep=N`` gives N blank lines before and after the
+    given output statement. ``vsep=(M,N)`` gives M blank lines before, and
+    N blank lines after. A new ``Vertical`` class describes vertical spacing
+    behind the scenes.
+  * ``Say`` no longer attempts to handle file encoding itself, but passes this
+    responsibility off to file objects, such as those returned by ``io.open``. This
+    is cleaner, though it does remove the whimsical
+    possibility of automagical base64 and rot13 encodings.
+    The ``encoding`` option is withdrawn as a result.
+  * You can now set the files you'd like to output to in the same way you'd set any other
+    option (e.g. ``say.set(files=[...])`` or ``say.clone(files=[...])``). "Magic" parameter
+    handling is enabled so that if any of the items listed are strings, then a file of that
+    name is opened for writing. Beware, however, that if you manage the files option
+    explicitly (e.g. ``say.options.files.append(...)``), you had better provide proper open files.
+    No magical interpretation is done then. The previously-necessary
+    ``say.setfiles()`` API remains, but is now deprecated.
+  * ``fmt()`` is now handled by ``Fmt``, a proper subclass of ``Say``, rather
+    than just through instance settings.
+  * ``say()`` no longer returns the
+    value it outputs. ``retvalue`` and ``encoded`` options have therefore been withdrawn.
+
 1.0.4 (September 16, 2013)
 ''''''''''''''''''''''''''
 
-  * Had to back out part of the common ``__version__`` grabbing. 
+  * Had to back out part of the common ``__version__`` grabbing.
     Not compatible
     with Sphinx / readthedocs build process.
 

File docs/conf.py

 # The short X.Y version.
 version = '1.0'
 # The full version, including alpha/beta/rc tags.
-release = '1.0.5'
+release = '1.1.0'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.

File docs/index.rst

 The more items that are being printed, and the complicated the ``format``
 invocation, the more valuable this simple inline specification becomes.
 
-Full
-expressions are are supported within the format braces (``{}``).
-Whatever variable names or expressions are found therein
-will be evaluated in the context of the caller.
+Full expressions are are supported within the format braces (``{}``). Whatever
+variable names or expressions are found therein will be evaluated in the context
+of the caller.
+
 
 Indentation and Wrapping
 ========================
 using Python's standard ``textwrap`` module.
 Feel free to use indentation and wrapping together.
 
-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.
-And it won't be integrated with wrapping.
-
-``say``, however, simply handles the indent level and wrapping, and it properly
-handles the multi-line string case. 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 additional higher-level formatting options.
-For now: indentation and wrapping.
-
 Prefixes and Suffixes
 =====================
 
     from icolor import cformat
     say(text, prefix=cformat('#BLUE;> '))
 
+And if you like your output numbered:
+
+    say.set(prefix=numberer())
+    assert say('this\nand\nthat')
+
+yields::
+
+      1: this
+      2: and
+      3: that
+
+The Value Proposition
+=====================
+
+While it's easy enough to add a few spaces to the format string of any ``print``
+statement or function in order to achieve a little indentation, 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 it won't take
+multi-line strings into account. Nor will it be integrated with line wrapping
+or numbering or other formatting you also want.
+
+``say``, however, simply and correctly handles these combined formatting
+operations. Harder cases like multi-line strings are just as nicely and well
+indented as simple ones--something not otherwise easily accomplished without
+adding gunky, complexifying string manipulation code to every place in your
+program that prints anything.
+
+This starts to illustrate ``say``'s "do the right thing" philosophy. So many
+languages' printing and formatting functions "output values" at a low level.
+They may format basic data types, but they don't provide straightforward ways to
+do neat text transformations that rapidly yield correct, attractively-formatted
+ouput. ``say`` does. Over time, ``say`` will provide even more high-level
+formatting options. For now: indentation, wrapping, and line numbering.
+
+.. note:: If you do find any errors in the way ``say`` handles formatting operations,
+`there's an app for that <https://bitbucket.org/jeunice/say/issues>`_. Let's fix
+them once, in a common place, in reusable code--not spread around many different programs.
+
 Titles and Horizontal Rules
 ===========================
 
 Good options for the separator might be be '-', '=', or parts of the `Unicode
 box drawing character set <http://en.wikipedia.org/wiki/Box-drawing_character>`_.
 
+Vertical Separation
+===================
+
+You don't need to add newline characters here and there to achieve good
+vertical spacing.  ``say.blank_lines(n)`` emits n blank lines. And just
+about every ``say`` call also supports a ``vsep`` (vertical separation)
+parameter.::
+
+    say('TITLE', vsep=(2,0)        # add 2 newlines before (none after)
+    say('=====', vsep=(0,2))       # add 2 newlines after (none before)
+    say('something else', vsep=1)  # add 1 newline before, 1 after
+
 Where You Like
 ==============
 
     import sys
     from say import say
 
-    say.setfiles([sys.stdout, "report.txt"])
+    say.set(files=[sys.stdout, "report.txt"])
     say(...)   # now prints to both sys.stdout and report.txt
 
-This has the advantage of allowing you to both capture and see
-program output, without changing
-any code (other than the config statement). You can also define your own targeted ``Say`` instances::
+This has the advantage of allowing you to both capture and see program output,
+without changing any code (other than the config statement). You can also define
+your own targeted ``Say`` instances::
 
-    from say import say, Say, stderr
+    import sys
+    from say import say
 
-    err = say.clone().setfiles([stderr, 'error.txt'])
-    err("Failed with error {errcode}")  # writes to stderr, error.txt
-
-Note that ``stdout`` and ``stderr`` are just convenience aliases to
-the respective
-``sys`` equivalents.
+    err = say.clone(files=[sys.stderr, 'error.txt'])
+    err("Failed with error {errcode}")  # writes in both places
 
 When You Like
 =============
 Encodings
 =========
 
-``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 formatted strings, ``say`` handles this by encoding
-into ``utf-8``.
+Character encodings remain a fractious and often exasperating part of IT.
+``say()`` and ``fmt()`` try to avoid this by working with Unicode strings. In
+Python 3, all strings are Unicode strings, and output is by default UTF-8
+encoded. Yay!
 
-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.)
-
-You can get creative with the encoding::
-
-    say('I am a truck!', encoding='base64')  # SSBhbSBhIHRydWNrIQo=
-
-Or change the default::
-
-    say.set(encoding='rot-13')
-
-Knock yourself out with `all the exciting opportunites
-<http://docs.python.org/library/codecs.html#standard-encodings>`_!
-If you really want the formatted text returned just as it is written to files,
-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
-what you need if you're going to ``print`` or
-``say`` the resulting text without a gratuitous "extra" newline.
+In Python 2, we try to maintain the same environemnt. If a template or input
+string is *not* of type ``unicode``, please include only ASCII characters, not
+encoded bytes from UTF-8 or whatever. If you don't do this, any trouble results
+be on your head. If ``say`` opens a file for you (e.g. with ``setfiles()``), it
+uses ``io.open()`` to inherit its default encoding to UTF-8. If you have ``say``
+write to a file that you've opened, you should similarly use ``io.open()`` or
+another mechanism that transparently writes to a proper encoding.
 
 Non-Functional Invocation
 =========================

File say/__init__.py

-from say.core import say, Say, fmt, caller_fmt, stdout, stderr, FmtException, numberer
+from say.core import say, Say, fmt, Fmt, caller_fmt, FmtException, numberer
 from say.vertical import Vertical
 from say.text import Text
 from say.version import __version__
             raise SyntaxError("syntax error when formatting '{0}'".format(s))
 
     if is_string(arg):
+        arg = unicode(arg) if six.PY2 and isinstance(arg, str) else arg
         parts = []
         for (literal_text, field_name, format_spec, conversion) in sformatter.parse(arg):
             parts.append(literal_text)
         return str(seval(str(arg)))
 
 
-
-
 ### Core Say class
 
 class Say(object):
     )
 
     options.magic(
-        indent = lambda v, cur: cur.indent + int(v) if isinstance(v, str) else v
+        indent = lambda v, cur: cur.indent + int(v) if isinstance(v, str) else v,
+        files  = lambda v, cur: opened(v)
     )
 
     def __init__(self, **kwargs):
         options (and thence to Say.options). kwargs become the cloned instance's
         overlay options.
         """
-        cloned = Say()
+        cloned = self.__class__()
         cloned.options = self.options.push(kwargs)
         return cloned
 
         else:
             outstr = self._outstr(data, opts)
             for f in opts.files:
-                f.write(s)
+                f.write(outstr)
 
 
 class SayContext(OptionsContext):
         silent=Prohibited,  # Fmt is never silent
     )
 
+    def __init__(self, **kwargs):
+        self.options = Fmt.options.push(kwargs)
+
     def _output(self, data, opts):
         """
         Construct the output string and return it.
 import types
 if six.PY3:
     from codecs import getencoder
-
+    unicode = str
+import io
 
 def is_string(v):
     """
     """
     return isinstance(v, six.string_types)
 
+def opened(f):
+    """
+    If f is a string, consider it a file name and return it, ready for writing.
+    Else, assume it's an open file. Just return it.
+    """
+    if isinstance(f, list):
+        return [ opened(ff) for ff in f ]
+    else:
+        return io.open(f, "w") if is_string(f) else f
+        # NB use io.open not plain open to get auto-encoding to UTF-8 in Python 2
 
 def encoded(u, encoding):
     """

File say/version.py

-__version__ = '1.0.5'
+__version__ = '1.1.0'

File test/test.py

 import six
 import sys
 import os
-from say import Say, fmt, stdout, caller_fmt, FmtException
+import io
+from say import *
 import pytest
 
 globalvar = 99
 
 
-def setup(**kwargs):
-    global say
-    global fmt
-    kwargs.setdefault('retvalue', True)
-    print kwargs
-    say = Say(**kwargs)
-    fmt_options = say.options.push(dict(encoding=None, retvalue=True, return_strip_newline=True, silent=True))
-    fmt_options.set(**kwargs)
-    fmt = Say(**fmt_options)
-    fmt.setfiles([])
-
-
 def test_basic(param='Yo'):
-    setup()
+    say = Fmt()
 
     greeting = "hello"
     assert say("{greeting}, world!") == "{0}, world!".format(greeting)
 
 
 def test_gt():
-    setup()
+    say = Fmt()
 
     x = 555
     assert (say > "{x} is a big number!") == "555 is a big number!"
 
-
-def test_hr_and_title():
-    setup()
-
-    say.title('say testing follows')
-    say.hr(vsep=1)
-
-    # http://en.wikipedia.org/wiki/Box-drawing_character
-    say.hr(sep=six.u('\u2504'))
-    say.hr(sep=six.u('\u2509'))
-    say.hr(sep=six.u('\u2550'))
-    say.hr(sep=six.u('\u2591'))
-    say.hr(sep=six.u('\u25EF '))
-    say.hr(sep=six.u('\u25C9 '))
-
-
 def test_localvar():
-    setup()
+    say = Fmt()
 
     m = "MIGHTY"
     assert say(m) == "MIGHTY"
     assert say(globalvar) == "tasty"
     assert say("{globalvar}") == "tasty"
 
-
 def test_globalvar():
-    setup()
+    say = Fmt()
 
     assert say("{globalvar}") == str(globalvar)
 
-
 def test_unicode():
-    setup()
+    say = Fmt()
 
     u = six.u('This\u2014is Unicode!')
     assert say(u) == u
     assert say(six.u("Unicode templates {too}")) == six.u("Unicode templates too")
 
 def test_format_string():
-    setup()
+    say = Fmt()
 
     x = 33.123456789
     assert say("{x} is floating point!") == '33.123456789 is floating point!'
     assert say("{x:8.3f} is in the middle") == '  33.123 is in the middle'
 
 
-def test_fmt():
-    setup()
-
-    v = 1212
-
-    assert say("++{v}") == fmt("++{v}")
-    # should print only once
-
-
-def test_encoded_encoding():
-    setup()
-    say.set(encoding='base64', encoded=True)
-    assert say('I am a truck!') == "SSBhbSBhIHRydWNrIQo="
-    assert say('I am a truck!', encoding='rot-13') == 'V nz n gehpx!'
-    assert say('V nz n gehpx!', encoding='rot-13') == 'I am a truck!'
-
-
 def test_files(capsys, tmpdir):
     say = Say()
 
     tmpfile = tmpdir.join('test.txt')
     say.setfiles([sys.stdout, tmpfile])
-    # this tweak needed to make pytest's capsys work right
-    # (in general, would use stdout alias, not sys.stdout)
 
     text = "Yowza!"
     say(text)
     assert tmpfile.read() == text + "\n"
     assert capsys.readouterr()[0] == text + "\n"
 
+    text2 = six.u('Hello\u2012there')
+    tmpfile2 = tmpdir.join('test2.txt')
+
+    tfname = tmpfile2.strpath
+    say.set(files=[sys.stdout, tfname])
+    say(text2)
+    tf = say.options.files[1]
+    tf.close()
+    with io.open(tfname, 'r') as tf2:
+        assert tf2.read() == text2 + "\n"
+    assert capsys.readouterr()[0] == text2 + "\n"
+
+    errfile = tmpdir.join('error.txt')
+    errcode = 12
+    err = say.clone(files=[sys.stderr, errfile])
+    err("Failed with error {errcode}")  # writes to stderr, error.txt
+    assert capsys.readouterr()[1] == 'Failed with error 12\n'
+    assert errfile.read()         == 'Failed with error 12\n'
+
+
 def test_example_1():
-    setup()
+    say = Fmt()
 
     x = 12
     nums = list(range(4))
     assert say("Nums has {len(nums)} items: {nums}") == "Nums has 4 items: [0, 1, 2, 3]"
 
 def test_example_2():
-    setup()
+    say = Fmt()
 
     errors = [{'name': 'I/O Error', 'timestamp': 23489273},
               {'name': 'Compute Error', 'timestamp': 349734289}]
 
 
 def test_say_silent():
-    setup()
+    say = Fmt()
     r1 = say("this is output")
     r2 = say("this is output", silent=True)
     assert r1 == r2
 
 
 def test_indent():
-    setup()
+    say = Fmt()
     assert say('no indent') == 'no indent'
     assert say('no indent', indent=0) == 'no indent'
     assert say('one indent', indent='1') == '    one indent'
 
 
 def test_sep_end():
-    setup()
+    say = Fmt()
     assert say(1, 2, 3) == '1 2 3'
     assert say(1, 2, 3, sep=',') == '1,2,3'
     assert say(1, 2, 3, sep=', ') == '1, 2, 3'
 
 
 def test_indent_special():
-    setup()
+    say = Fmt()
     say.set(indent_str='>>> ')
     assert say('something') == 'something'
     assert say('else', indent=1) == '>>> else'
 
 
 def test_indent_multiline():
-    setup()
+    say = Fmt()
     assert say('and off\nwe go', indent='+1') == '    and off\n    we go'
 
 
 def test_with_indent():
-    setup()
+    say = Fmt()
     with say.settings(indent='+1'):
         assert say("I am indented!") == "    I am indented!"
         with say.settings(indent='+1'):
     assert say('back again') == 'back again'
 
 def test_prefix_suffix():
-    setup()
+    say = Fmt()
     assert say('x', prefix='<', suffix='>') == '<x>'
     assert say('x', end='\n--') == 'x\n--'
     assert say('a\nb\nc', prefix='> ') == '> a\n> b\n> c'
     assert quoter('a\nb\nc') == '> a\n> b\n> c'
 
 def test_prefix_in_context_example():
-    setup()
+    say = Fmt()
     with say.settings(prefix='> '):
         assert say('this') == '> this'
         assert say('that') == '> that'
     assert say('other') == 'other'
 
 def test_colored_output():
-    setup()
+    say = Fmt()
     with say.settings(prefix='\x1b[34m', suffix='\x1b[0m'):
         assert say('this is blue!') == '\x1b[34mthis is blue!\x1b[0m'
     assert say('not blue') == 'not blue'
     assert blue('BLUE') == '\x1b[34mBLUE\x1b[0m'
 
 def test_example_3():
-    setup()
+    say = Fmt()
     items = '1 2 3'.split()
 
     assert say('TITLE') == 'TITLE'
 
 
 def test_wrap():
-    setup()
-    assert say('abc\ndef\nghi', wrap=79) == 'abc def ghi'
+    say = Fmt()
+    assert say('abc\ndef\nghi', wrap=79) == 'abc\ndef\nghi'
     assert say("abcde abcde abcde", wrap=6) == 'abcde\nabcde\nabcde'
     assert say("abcde abcde abcde", wrap=10, indent=1) == '    abcde\n    abcde\n    abcde'
 
 def test_vsep():
-    setup(return_strip_newline=False)
-
-    assert say.title('hey', sep='-', vsep=2) == '\n\n\n--------------- hey ---------------\n\n\n'
-    assert fmt.title('hey', sep='-', vsep=2) == '\n\n\n--------------- hey ---------------\n\n\n'
+    say = Fmt(end='\n')
+    assert say.title('hey', sep='-', vsep=2) == '\n\n--------------- hey ---------------\n\n\n'
 
 def test_hr():
-    setup(return_strip_newline=False)
-    assert say.hr(sep='-') == '\n----------------------------------------'
-    assert fmt.hr(sep='-') == '\n----------------------------------------'
+    say = Fmt(end='\n')
+    assert say.hr(sep='-') == '----------------------------------------\n'
+    assert say.hr(sep='-', vsep=2) == '\n\n----------------------------------------\n\n\n'
 
 def test_blank_lines():
-    setup(return_strip_newline=False)
+    say = Fmt(end='\n')
 
     assert say.blank_lines(3) == '\n' * 3
-    assert fmt.blank_lines(3) == '\n' * 3
+
+def test_numberer():
+    say = Fmt(end='\n', prefix=numberer())
+    assert say('this\nand\nthat') == '  1: this\n  2: and\n  3: that\n'
+
+    say = Fmt(end='\n', prefix=numberer(template='{n:>4d}: '))
+
+    assert say('this\nand\nthat') == '   1: this\n   2: and\n   3: that\n'
 
 def test_caller_fmt():
 

File test/test_util.py

 
 import six
 from say.util import *
+import pytest
 
 
 def test_is_string():
     assert not is_string([1, 2, 3])
     assert not is_string(['a', 'b', 'c'])
 
+@pytest.mark.xfail
+def test_opened():
+    raise NotImplementedError('TBD')
 
 def test_encoded():