Commits

Jonathan Eunice committed ed4b39e

Added styles and ANSI colors. Pushing 1.2.0

  • Participants
  • Parent commits bf27f4a

Comments (0)

Files changed (13)

 Changes
 =======
 
+1.2.0 (September 30, 2013)
+''''''''''''''''''''''''''
+
+  * Added style definitions and convenient access to ANSI colors.
+
+
 1.1.0 (September 24, 2013)
 ''''''''''''''''''''''''''
 
 
 The more items being printed, and the more complicated the ``format``
 invocation, the more valuable this simple inline specification becomes.
+As more formatting functions come online, the complexity gap widens. For
+example::
+
+    say("His name is {name:style='red+bold'}")
+
+Or::
+
+    say.style(stars=lambda x: fmt('*** ', style='red') + \
+                              fmt(x,      style='black') + \
+                              fmt(' ***', style='red'))
+
+    say(message, style='stars')
+
 
 Beyond DRY, Pythonic templates that piggyback the
 Python's well-proven ``format()`` method, syntax, and underlying engine,
     titles,
     horizontal separators, and
     vertical whitespace.
+  * Convenient access to style-driven formatting, ANSI colors, and other
+    text transformations/post-processing.
   * Easy output to one or more files, with no additional code.
   * Super-duper template/text aggregator objects for easily building,
     reading, and writing multi-line texts.

File docs/CHANGES.rst

 Changes
 =======
 
+1.2.0 (September 30, 2013)
+''''''''''''''''''''''''''
+
+  * Added style definitions and convenient access to ANSI colors.
+
 1.1.0 (September 24, 2013)
 ''''''''''''''''''''''''''
 

File docs/conf.py

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

File docs/index.rst

     indentation, and wrapping built in.
   * Convenient methods for common formatting items such as titles, horizontal
     separators, and vertical whitespace.
+  * Convenient access to style-driven formatting, ANSI colors, and other
+    text transformations/post-processing.
   * Easy output to one or more files, with no additional code.
   * Super-duper template/text aggregator objects for easily building,
     reading, and writing multi-line texts.
 that works correctly from Python 2.6 forward from
 a single code base.)
 
-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.
 
+The more items that are being printed, and the complicated the ``format``
+invocation, the more valuable this simple inline specification becomes.
+
+The more items being printed, and the more complicated the ``format``
+invocation, the more valuable this simple inline specification becomes.
 
 Indentation and Wrapping
 ========================
 
 Or if you'd like some text to be quoted with blue quotes::
 
-    from icolor import cformat
-    say(text, prefix=cformat('#BLUE;> '))
+    say(text, prefix=styled('> ', 'blue'))
 
 And if you like your output numbered::
 
 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
+output. ``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,
     say('=====', vsep=(0,2))       # add 2 newlines after (none before)
     say('something else', vsep=1)  # add 1 newline before, 1 after
 
+Colors and Styles
+=================
+
+``say`` has built-in support for style-driven formatting. By default,
+ANSI terminal colors and styles are automagically supported.
+
+::
+
+    answer = 42
+
+    say("The answer is {answer:style=bold+red}")
+
+This uses the `ansicolors <https://pypi.python.org/pypi/ansicolors>`_ module,
+though with a slightly more permissive syntax. Available colors are
+'black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', and 'yellow'. Available
+styles are 'bold', 'italic', 'underline', 'blink', 'blink2', 'faint', 'negative', 'concealed', and 'crossed'
+(though not all styles are available on every terminal).
+
+Or if you want to define your own styles::
+
+    say.style(redwarn=lambda n: color(n, fg='red', style='bold') if int(n) < 0 else n)
+    ...
+    say("Result: {n:style=redwarn}")
+
+You can also apply a style to the entire value::
+
+    say(n, style='green|underline')
+
+While the goal of ``say`` is to have correct behavior under absolutely all
+combinations of text styling, coloring, indentation, numbering, and so on, be
+aware that the coloring/styling is relatively very new, has limited tests and
+documentation to
+date, and is still evolving.
+One known bug attends ``say``'s use of Python's ``textwrap`` module,
+which is not
+savvy to ANSI-terminal control codes; text that includes control
+codes and is wrapped is currently likely to wrap in the wrong place.
+Enclosing one bit of colored text inside another bit of colored text
+is not as easy as it could be.
+Finally, style definitions are
+idiosyncratically shared across instances. That said, some fairly complex
+invocations already work quite nicely. Try, e.g.::
+
+    say.set(prefix=numberer(template=color('{n:>3}: ', fg='blue')), \
+            wrap=40)
+    say('a long paragraph...with gobs of text', style='red')
+
+This correctly puts the line numbers in blue, wraps the lines to 40 characters,
+and puts the text in red. (The ``textwrap`` collision with control characters
+is avoided here because the wrapped text is pure, and the control codes for
+red styling are added after wrapping.)
+
+Styled formatting is an extremely powerful approach, giving the
+same kind of flexibity and abstraction seen for styles in word processors and
+CSS-based Web design. It will be further developed here.
+Plans already include replacing ``textwrap`` with an ANSI-savvy text wrapping
+module, providing simpler ways to state complex formatting, and mechanisms
+to auto-map styles into HTML output.
+
 Where You Like
 ==============
 
 Python 3, all strings are Unicode strings, and output is by default UTF-8
 encoded. Yay!
 
-In Python 2, we try to maintain the same environemnt. If a template or input
+In Python 2, we try to maintain the same environment. 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
 
 Often the job of output is not about individual text lines, but about creating
 multi-line files such as scripts and reports. This often leads away from standard
-output mechanisms toward template pakcages, but ``say`` has you covered here as
+output mechanisms toward template packages, but ``say`` has you covered here as
 well.
 
 ::
 
 ``Text`` objects are basically a list of text lines. In most cases, when you add
 text (either as multi-line strings or lists of strings), ``Text`` will
-automatically interopolate variables the same way ``say`` does. One can
+automatically interpolate variables the same way ``say`` does. One can
 simply ``print`` or
 ``say`` ``Text`` objects, as their ``str()`` value is the full text you would
 assume. ``Text`` objects have both ``text`` and ``lines`` properties which
 text object, with optional interpolation and dedenting). One can also
 ``write_to()`` a file. Use the ``append`` flag if you wish to add to rather than
 overwrite the file of a given name, and you can set an output encoding if you
-like (``encoding='utf=8'`` is the default).
+like (``encoding='utf-8'`` is the default).
 
 So far we've discussed``Text`` objects almost like strings, but they also act
 as lists of individual lines (strings). They are, for example,
-indexible via ``[]``, and they are iterable.
+indexable via ``[]``, and they are iterable.
 Their ``len()`` is the number of lines they contain. One can
 ``append()`` or ``extend()`` them with one or multiple strings, respectively.
 ``append()`` takes a keyword parameter ``interpolate`` that controls whether
 a ``dict``, all the keys will be replaced with their corresponding values.
 
 ``Text`` doesn't have the full set of text-onboarding options seen in `textdata
-<http://pypi.python.org/pypi/textdata>`_, but it should suit many cirumstances.
+<http://pypi.python.org/pypi/textdata>`_, but it should suit many circumstances.
 If you need more, ``textdata`` can be used alongside ``Text``.
 
 
 
 You may want to write your own functions that take strings
 and interpolate ``{}``
-format tempaltes in them. The easy way is::
+format templates in them. The easy way is::
 
     from say import caller_fmt
 
 software that should work across the versions, without the hassle
 of ``from __future__ import print_function``.
 
-``say`` attempts to mask some of the quirky compexities of the 2-to-3 divide,
+``say`` attempts to mask some of the quirky complexities of the 2-to-3 divide,
 such as string encodings and codec use.
 
 Alternatives
    similar to ``say.fmt()``, in that it can
    interpolate complex Python expressions, not just names.
    It's ``i % "format string"`` syntax is a little odd, however, in
-   the way that it repurposes Python's earlier ``"C format string" % (values)``
+   the way that it re-purposes Python's earlier ``"C format string" % (values)``
    style ``%`` operator. It also depends on the native ``print`` statement
    or function, which doesn't help bridge Python 2 and 3.
 
  *  Automated multi-version testing is managed with the wonderful
     `pytest <http://pypi.python.org/pypi/pytest>`_
     and `tox <http://pypi.python.org/pypi/tox>`_. ``say`` is now
-    successfully packaged for, and tested against, all late-model verions of
+    successfully packaged for, and tested against, all late-model versions of
     Python: 2.6, 2.7, 3.2, and 3.3, as well as PyPy 2.1 (based on 2.7.3).
 
  *  ``say`` has greater ambitions than just simple template printing. It's part

File say/__init__.py

 from say.core import say, Say, fmt, Fmt, caller_fmt, FmtException, numberer
+from say.styling import color, COLORS, STYLES, autostyle, styled
 from say.vertical import Vertical
 from say.text import Text
 from say.version import __version__
 import six
 import textwrap
 from say.util import *
+from say.styling import *
 from say.vertical import Vertical, vertical
 
 if six.PY3:
 
 sformatter = string.Formatter()  # piggyback Python's format() template parser
 
+def populate_style(style_name, styles):
+    if style_name.startswith(("'", '"')):
+        style_name = style_name[1:-1]
+    if style_name not in styles:
+        styles[style_name] = autostyle(style_name)
 
-def _sprintf(arg, caller, override=None):
+
+def _sprintf(arg, caller, styles=None, override=None):
     """
     Format the template string (arg) with the values seen in the context of the
     caller. If override is defined, it is a Mapping providing additional values
         except SyntaxError:
             raise SyntaxError("syntax error when formatting '{0}'".format(s))
 
+    def parse_style(fs, styles):
+        """
+        Get the style component out of the format string, if any.
+        """
+        if "style" in fs:
+            fs_parts = fs.split(',')
+            raw_fs_parts = []
+            for fsp in fs_parts:
+                if fsp.startswith('style='):
+                    style_name = fsp[6:]
+                else:
+                    raw_fs_parts.append(fsp)
+            populate_style(style_name, styles)
+            return style_name, raw_fs_parts
+        else:
+            return None, fs
+
     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)
             if field_name is not None:
-                format_str = six.u("{0") + ("!" + conversion if conversion else "") + \
-                                          (":" + format_spec if format_spec else "") + "}"
+
+                style_name, raw_format_spec = parse_style(format_spec, styles)
+                format_str = six.u("{0") + \
+                                   ("!" + conversion if conversion else "") + \
+                                   (":" + raw_format_spec if raw_format_spec else "") + "}"
                 field_value = seval(field_name)
                 formatted = format_str.format(field_value)
+                if style_name and style_name in styles:
+                    formatted = styles[style_name](formatted)
+
                 parts.append(formatted)
         return ''.join(parts)
     else:
         vsep=None,          # vertical separation
         end='\n',           # end output with this (Python print function compatible)
         silent=False,       # be quiet
+        style=None,         # name of style in which to wrap entire output line
+        styles={},          # style dict
         _callframe=Transient, # frome from which the caller was calling
     )
 
         opts = self.options.push(kwargs)
         opts.setdefault('_callframe', inspect.currentframe().f_back)
 
-        formatted = _sprintf(name, opts._callframe) if is_string(name) else str(name)
+        formatted = _sprintf(name, opts._callframe, opts.styles) if is_string(name) else str(name)
         bars = sep * width
         line = ' '.join([bars, formatted, bars])
         return self._output(line, opts)
         cloned.options = self.options.push(kwargs)
         return cloned
 
+    def style(self, *args, **kwargs):
+        """
+        Define a style.
+        """
+        for k,v in kwargs.items():
+            if isinstance(v, six.string_types):
+                self.options.styles[k] = autostyle(v)
+            else:
+                self.options.styles[k] = v
+
+
     def __call__(self, *args, **kwargs):
         """
         Primary interface. say(something)
         opts = self.options.push(kwargs)
         opts.setdefault('_callframe', inspect.currentframe().f_back)
 
-        formatted = [ _sprintf(arg, opts._callframe) if is_string(arg) else str(arg)
+        formatted = [ _sprintf(arg, opts._callframe, opts.styles) if is_string(arg) else str(arg)
                       for arg in args ]
         return self._output(opts.sep.join(formatted), opts)
 
         opts = self.options.push({})
         opts.setdefault('_callframe', inspect.currentframe().f_back)
 
-        formatted = [ _sprintf(arg, opts._callframe) if is_string(arg) else str(arg) ]
+        formatted = [ _sprintf(arg, opts._callframe, opts.styles) if is_string(arg) else str(arg) ]
         return self._output(opts.sep.join(formatted), opts)
 
     def _return_value(self, outstr, opts):
         """
         datalines = data if isinstance(data, list) else data.splitlines()
 
-        if opts.indent or opts.wrap or opts.prefix or opts.suffix or opts.vsep:
+        if opts.indent or opts.wrap or opts.prefix or opts.suffix or opts.vsep or opts.style:
             indent_str = opts.indent_str * opts.indent
             if opts.wrap:
                 datastr = '\n'.join(datalines)
                 datalines = []
                 for line in wrappedlines:
                     datalines.extend(line.splitlines())
+            if opts.style:
+                styler = opts.styles.get(opts.style, None)
+                if not styler:
+                    styler = opts.styles.setdefault(opts.style, autostyle(opts.style))
+                datalines = [styler(line) for line in datalines]
             if opts.indent:
                 datalines = [ indent_str + line for line in datalines ]
             vbefore, vafter = vertical(opts.vsep).render()
 
     def __init__(self, **kwargs):
         self.options = Fmt.options.push(kwargs)
+        self.options.styles = say.options.styles  # styles are idiosyncratially shared
+
 
     def _output(self, data, opts):
         """

File say/styling.py

+"""
+Module to handle styled printing
+"""
+
+import six
+from colors import color as _color, COLORS, STYLES
+if six.PY3:
+    unicode = str
+
+class optdict(dict):
+    """
+    dict subclass that only initializes those keys where there's a non-empty value.
+    Convenient for creating kwarg dicts that don't have to define default values.
+    """
+    def __init__(self, **kwargs):
+        dict.__init__(self)
+        for k,v in kwargs.items():
+            if v:
+                self[k] = v
+
+def styledef(*args, **kwargs):
+    """
+    Return a lambda that implements the given style definition.
+    First named color is foreground, second is background. Styles
+    can be named anywhere.
+    """
+    kw = {}
+    for arg in args:
+        arg = arg.replace('+', '|').replace(',', '|').lower()
+        parts = [p.strip() for p in arg.split('|')]
+        fg, bg, styles = None, None, []
+        for p in parts:
+            if p in COLORS:
+                if not fg:
+                    fg = p
+                elif not bg:
+                    bg = p
+                else:
+                    raise ValueError('only fg and bg colors!')
+            elif p in STYLES:
+                styles.append(p)
+        kw.update(optdict(fg=fg, bg=bg, style='|'.join(styles) if styles else None))
+    kw.update(kwargs)
+    return kw
+
+def autostyle(*args, **kwargs):
+    """
+    Return a lambda that will later format a string with the given styledef.
+    """
+    sdef = styledef(*args, **kwargs)
+    return lambda x: color(x, **sdef)
+
+def color(item, **kwargs):
+    """
+    Like colors.color, except auto-casts to Unicode string if need be.
+    """
+    item_u = unicode(item) if not isinstance(item, six.string_types) else item
+    return _color(item_u, **kwargs)
+
+def styled(item, *args, **kwargs):
+    """
+    Like color, but can also include style names.
+    Kind of an immediate-mode version of autostyle.
+    """
+    sdef = styledef(*args, **kwargs)
+    return color(item, **sdef)
 import sys
 import six
 import types
+import csv
+import io
 if six.PY3:
     from codecs import getencoder
     unicode = str
-import io
+    from io import StringIO
+else:
+    from StringIO import StringIO
 
 def is_string(v):
     """
     except TypeError:
         value = g if g is not None else ''
     return unicode(value)
+

File say/version.py

-__version__ = '1.1.0'
+__version__ = '1.2.0'
     long_description=open('README.rst').read(),
     url='https://bitbucket.org/jeunice/say',
     packages=['say'],
-    install_requires=['six>=1.4.1', 'options>=1.0.3', 'stuf>=0.9.12', 'simplere>=1.0', 'mementos>=1.0'],
+    install_requires=['six>=1.4.1', 'options>=1.0.3', 'stuf>=0.9.12', 'simplere>=1.0', 'mementos>=1.0',
+                      'ansicolors'],
     tests_require = ['tox', 'pytest', 'six'],
     zip_safe = True,
     keywords='print format template interpolate say',

File test/test_styling.py

+
+from say import *
+import six
+
+def test_basic_styling():
+    fmt = Fmt()
+
+    assert fmt('this', style='green+underline') == six.u('\x1b[32;4mthis\x1b[0m')
+    assert fmt('this', style='bold+red') == six.u('\x1b[31;1mthis\x1b[0m')
+
+
+def test_readme_example():
+    fmt = Fmt()
+    fmt.style(stars=lambda x: fmt('*** ', style='red') + \
+                              fmt(x,      style='black') + \
+                              fmt(' ***', style='red'))
+
+    message = 'Warning, problem!'
+    assert fmt(message, style='stars') == six.u('\x1b[31m*** \x1b[0m\x1b[30mWarning, problem!\x1b[0m\x1b[31m ***\x1b[0m')
+
+def test_wrapping_example():
+    fmt = Fmt()
+    text = "Move over, Coke. It looks like Apple is the real thing. The tech giant has ended Coca-Cola's 13-year run as the world's most valuable brand on a highly regarded annual list."
+
+    fmt = Fmt()
+    fmt.set(prefix=numberer(template=color('{n:>3}: ', fg='blue')), \
+            wrap=40)
+    assert fmt(text) == six.u("\x1b[34m  1: \x1b[0mMove over, Coke. It looks\n\x1b[34m  2: \x1b[0mlike Apple is the real\n\x1b[34m  3: \x1b[0mthing. The tech giant has\n\x1b[34m  4: \x1b[0mended Coca-Cola's 13-year\n\x1b[34m  5: \x1b[0mrun as the world's most\n\x1b[34m  6: \x1b[0mvaluable brand on a highly\n\x1b[34m  7: \x1b[0mregarded annual list.")
+
+    # now reset so numbering starts back at 1
+    fmt = Fmt()
+    fmt.set(prefix=numberer(template=color('{n:>3}: ', fg='blue')), \
+            wrap=40)
+    assert fmt(text, style='red') == six.u("\x1b[34m  1: \x1b[0m\x1b[31mMove over, Coke. It looks\x1b[0m\n\x1b[34m  2: \x1b[0m\x1b[31mlike Apple is the real\x1b[0m\n\x1b[34m  3: \x1b[0m\x1b[31mthing. The tech giant has\x1b[0m\n\x1b[34m  4: \x1b[0m\x1b[31mended Coca-Cola's 13-year\x1b[0m\n\x1b[34m  5: \x1b[0m\x1b[31mrun as the world's most\x1b[0m\n\x1b[34m  6: \x1b[0m\x1b[31mvaluable brand on a highly\x1b[0m\n\x1b[34m  7: \x1b[0m\x1b[31mregarded annual list.\x1b[0m")
+
+def test_color():
+    assert color('text', fg='green') == '\x1b[32mtext\x1b[0m'
+    assert color('more', fg='blue', bg='yellow') == '\x1b[34;43mmore\x1b[0m'
+
+def test_styled():
+    assert color('test', fg='green', style='bold') == styled('test', 'green+bold')
+    assert color('test', fg='green', style='bold') == styled('test', 'bold+green')
+    assert color('test', fg='green', bg='red', style='bold') == styled('test', 'green+red+bold')
+    assert color('test', fg='green', bg='red', style='bold') == styled('test', 'bold+green+red')
+    assert color('test', fg='green', bg='red', style='bold') == styled('test', 'bold|green|red')
+    assert color('test', fg='green', bg='red', style='bold') == styled('test', 'bold,green,red')
+
+
+def test_in_or_out():
+    fmt = Fmt()
+
+    x = 12
+    assert fmt(x, style='blue+white+underline') == fmt("{x:style=blue+white+underline}")
+
+    fmt.style(bwu=autostyle('blue+white+underline'))
+    fmt.style(bwu2='blue+white+underline')
+
+    assert fmt(x, style='bwu') == fmt(x, style='blue+white+underline')
+    assert fmt(x, style='bwu') == fmt(x, style='bwu2')

File test/test_util.py

 
     for gg in [ 1, 1.1, 'string', list, [1,2,3], {'a':'A'} ]:
         assert next_str(gg) == str(gg)
+
+@pytest.mark.skipif('True')
+def test_csv_split():
+    # no quotes
+    assert csv_split('x,y,z') == ['x', 'y', 'z']
+
+    # double quotes
+    assert csv_split('x,y="simple",z') == ['x', 'y="simple"', 'z']
+    assert csv_split('x,y="internal comma,",z') == ['x', 'y="internal comma,"', 'z']
+
+    # single quotes
+    assert csv_split("x,y='simple',z") == ['x', "y='simple'", 'z']
+    assert csv_split("x,y='internal comma,',z") == ['x', "y='internal comma,'", 'z']