Commits

Jonathan Eunice committed 7593aa4

Fixed bug with quoted style names. Added tests. Tweaked docs. Prepared some for future Style objects and auto-joining of collections.

Comments (0)

Files changed (9)

 Changes
 =======
 
+1.2.1 (October 16, 2013)
+''''''''''''''''''''''''
+
+  * Fixed bug with quoting of style names/definitions. 
+  * Tweaked documentation of style definitions.
+
 1.2.0 (September 30, 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'}")
+One final example::
 
-Or::
-
-    say.style(stars=lambda x: fmt('*** ', style='red') + \
-                              fmt(x,      style='black') + \
-                              fmt(' ***', style='red'))
-
-    say(message, style='stars')
-
+    say.title('Discovered')
+    say("Name: {name:style=blue+underline}")
+    say("Age:  {age:style=blue}")
 
 Beyond DRY, Pythonic templates that piggyback the
 Python's well-proven ``format()`` method, syntax, and underlying engine,
   * A companion ``fmt()`` object for string formatting.
   * 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
-    vertical whitespace.
-  * Convenient access to style-driven formatting, ANSI colors, and other
-    text transformations/post-processing.
+  * Convenient methods for common formatting items such as titles, horizontal
+    separators, and vertical whitespace.
+  * Easy styled output, including ANSI colors and user-defined styles
+    and text transforms.
   * 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.
 Changes
 =======
 
+1.2.1 (October 16, 2013)
+''''''''''''''''''''''''
+
+  * Fixed bug with quoting of style names/definitions. 
+  * Tweaked documentation of style definitions.
+
 1.2.0 (September 30, 2013)
 ''''''''''''''''''''''''''
 
   * Added style definitions and convenient access to ANSI colors.
 
+
 1.1.0 (September 24, 2013)
 ''''''''''''''''''''''''''
 
     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 styled output, including ANSI colors and user-defined styles
+    and text transforms.
   * 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.
 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).
+styles are 'bold', 'italic', 'underline', 'blink', 'blink2', 'faint', 'negative', 'concealed', and 'crossed'.
+These styles can be combined with a ``+`` or ``|`` character.
+Note, however, that not all styles are available on every terminal.
 
-Or if you want to define your own styles::
+.. note:: When naming a style within the template braces (``{}``) of format strings, you can quote the style name or not. ``fmt("{x:style=red+bold}")`` is equivalent to ``fmt("{x:style='red+bold'}")``.
+
+You can define your own styles::
+
+    say.style(warning=lambda x: color(x, fg='red'))
+
+Because styles are defined through executables (lambdas, usually), they can
+include decisions or text transformations of arbitrary complexity.
+For example::
+
 
     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::
+That will display the number ``n`` in bold red charactes, but only if it's value is
+negative. For positive numbers, ``n`` is displayed normally.
 
-    say(n, style='green|underline')
+Or define a style where a message is surrounded by red stars::
+
+    say.style(stars=lambda x: fmt('*** ', style='red') + \
+                              fmt(x,      style='black') + \
+                              fmt(' ***', style='red'))
+    say.style(redacted=lambda x: 'x' * len(x))
+
+    message = 'hey'
+    say(message, style='stars')
+    say(message, style='redacted')
+
+Yields::
+
+    *** hey ***
+    xxx
+
+(with red stars)
+
+.. note:: Style defining lambdas (or functions) take string arguments. If the string is logically a number, it must be then cast into an ``int``, ``float``, or whatever. The code must ultimate return a string.
+
+You can also apply a style to the entire contents of a ``say`` or ``fmt`` invocation::
+
+    say("There is green everywhere!", 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
+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.::
 
             raw_fs_parts = []
             for fsp in fs_parts:
                 if fsp.startswith('style='):
-                    style_name = fsp[6:]
+                    style_name = fsp[6:].strip(QUOTE_DELIM_STR)
                 else:
                     raw_fs_parts.append(fsp)
             populate_style(style_name, styles)
         else:
             return None, fs
 
+        # TODO: Replace this parser with something more professional
+        # TODO: join=
+
     if is_string(arg):
         arg = unicode(arg) if six.PY2 and isinstance(arg, str) else arg
         parts = []
+"""
+Module to assist in the super-common operation of
+joining values in sequences into strings.
+"""
+
+import re
+from options import Options
+import six
+
+def is_string(v):
+    return isinstance(v, six.string_types)
+
+def stringify(v):
+    """
+    Return a string. If it's already a string, just return that.
+    Otherwise, stringify it.
+    """
+    return v if is_string(v) else str(v)
+
+def blanknone(v):
+    """
+    Return a value, or empty string if it's None.
+    """
+    return '' if v is None else v
+
+joiner_options = Options(
+    sep = ', ',    # separator between items
+    twosep=None,   # separator between items if only two
+    lastsep=None,  # separator between penultimate and final item
+    quoter=None,   # quoter for individual items
+    endcaps=None,  # quoter for entire joined sequence
+    prefix=None,   # prefix for entire joined, endcaped sequence
+    suffix=None    # suffix for entire joined, endcaped sequence
+)
+
+def joiner(seq, **kwargs):
+    """
+    Join the items of a sequence into a string. Implicitly stringifies any
+    not-string values. Allows specification of the separator between items (and
+    a special case for the last separator). Allows each item to be optionally
+    quoted by a function, and the entire list to be optionally quoted with an
+    endcaps function. A separate suffix and prefix may also be provdied.
+    """
+
+    opts = joiner_options.push(kwargs)
+
+    def prep(v):
+        """
+        Prepare an item by stringifying and optionally quoting it.
+        """
+        s = stringify(v)
+        return opts.quoter(s) if opts.quoter else s
+
+    seqlist = list(seq)
+    length = len(seqlist)
+    if length == 0:
+        core = ''
+    elif length == 1:
+        core = prep(seqlist[0])
+    elif length == 2 and opts.twosep:
+        sep = opts.twosep if opts.twosep is not None else opts.sep
+        core = sep.join(prep(v) for v in seqlist)
+    else:
+        start = [ prep(v) for v in seqlist[:-1] ]
+        final = prep(seqlist[-1])
+        if opts.lastsep is None:
+            opts.lastsep = opts.sep
+        core = opts.lastsep.join([ opts.sep.join(start), final])
+    capped = opts.endcaps(core) if opts.endcaps else core
+    if opts.prefix or opts.suffix:
+        affixed = ''.join([ blanknone(opts.prefix), capped, blanknone(opts.suffix) ])
+    else:
+        affixed = capped
+    return affixed
+
+def word_join(seq, **kwargs):
+    """
+    Slightly specific joiner for words. Returns by default an Oxford comma list,
+    but accepts all the same options as joiner.
+    """
+    kwargs.setdefault('sep', ', ')
+    kwargs.setdefault('twosep', ' and ')
+    kwargs.setdefault('lastsep', ', and ')
+    return joiner(seq, **kwargs)
+
+join = joiner
+and_join = word_join
+
+def or_join(seq, **kwargs):
+    """
+    A, B, or C.
+    """
+    kwargs.setdefault('sep', ', ')
+    kwargs.setdefault('twosep', ' or ')
+    kwargs.setdefault('lastsep', ', or ')
+    return joiner(seq, **kwargs)
+
+#def is_sequence(arg):
+#    return (not hasattr(arg, "strip") and
+#            hasattr(arg, "__getitem__") or
+#            hasattr(arg, "__iter__"))
+
+
+
+concat_options = joiner_options.add(
+    sep = '', # combine with no separator, by default
+)
+
+def concat(*args, **kwargs):
+    six.print_("kwargs:", kwargs)
+    six.print_("concat_options:", concat_options)
+    opts = concat_options.push(kwargs)
+    six.print_("opts:", opts)
+    six.print_("args:", args)
+
+    args=list(args)
+    six.print_("listed args:", args)
+
+    if len(args) == 1 and is_sequence(args):
+        args = list(args[0])
+    six.print_("args:", args)
+    six.print_("opts:", opts)
+    return joiner(args, **opts)  # problem here in Py3x
+
+items_options = Options(
+    sep = "\n",  # separator between items
+    fmt = "{key}: {value!r}",
+    header = None,   # header for entire list
+    footer = None    # footer for entire list
+)
+
+def iter_items(items):
+    if hasattr(items, 'items'):  # dict or mapping
+        for k, v in items.items():
+            yield k, v
+    else:
+        for k, v in enumerate(items):
+            yield k, v
+
+def items(seq, **kwargs):
+    opts = items_options.push(kwargs)
+
+    formatted_items = [ opts.fmt.format(key=k, value=v) for k,v in iter_items(seq) ]
+    items_str = opts.sep.join(formatted_items)
+    if opts.header or opts.footer:
+        parts = []
+        if opts.header:
+            parts.extend([opts.header, opts.sep])
+        parts.append(items_str)
+        if opts.footer:
+            parts.extend([opts.sep, opts.footer])
+        items_str = ''.join(parts)
+    return items_str
 
 import six
 from colors import color as _color, COLORS, STYLES
+from stuf import stuf
 if six.PY3:
     unicode = str
 
+QUOTE_DELIM = ('"', "'")
+QUOTE_DELIM_STR = ''.join(QUOTE_DELIM)
+
+# TODO: implement a persistent Style object that say.style will return
+# TODO: implement own six-like features, rather than hanging between six and custom code
+
 class optdict(dict):
     """
     dict subclass that only initializes those keys where there's a non-empty value.
             if v:
                 self[k] = v
 
+
+class StyleDef(object):
+    """
+    Class defining a formatting style.
+    """
+
+    def __init__(self, *args, **kwargs):
+        """
+        Create a new StyleDef, remembering its settings.
+        """
+        self.name = kwargs.get('name', None)
+        self.sdef = styledef(*args, **kwargs)
+        self.join = kwargs.get('join', None)
+
+    def __call__(self, item):
+        """
+        Apply this style to the given item.
+        """
+        result = self.join(item) if self.join else item
+        result = _color(result, **self.sdef) if self.sdef else result
+        return result
+
 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.
+    Parses a style definition as given in args and kwargs.
+    Return a dict that defines a given style definition. If it's an ANSI style
+    being defined, whatever color is named first (if any) is assumed to be the
+    foreground, the second the background. ANSI display styles (like 'bold') can
+    be named anywhere.
     """
-    kw = {}
+    kw = stuf()
     for arg in args:
+        # print "ARG:", arg
         arg = arg.replace('+', '|').replace(',', '|').lower()
         parts = [p.strip() for p in arg.split('|')]
         fg, bg, styles = None, None, []
                 styles.append(p)
         kw.update(optdict(fg=fg, bg=bg, style='|'.join(styles) if styles else None))
     kw.update(kwargs)
+    # print "KW:", kw
     return kw
 
 def autostyle(*args, **kwargs):

test/test_joiner.py

+
+from say.joiner import *
+import pytest, sys
+
+### Helper functions
+
+def single(s):
+    return "'" + s + "'"
+
+def brackets(s):
+    return "[" + s + "]"
+
+
+### Tests
+
+def test_oxford():
+    assert word_join([1,2,3]) == '1, 2, and 3'
+
+def test_heathen():
+    assert word_join([1,2,3], lastsep=" and ") == '1, 2 and 3'
+
+def test_quoted():
+    assert word_join([], quoter=single) == ""
+    assert word_join([1], quoter=single) == "'1'"
+    assert word_join([1,2], quoter=single) == "'1' and '2'"
+    assert word_join([1,2,3], quoter=single) == "'1', '2', and '3'"
+
+def test_listy():
+    assert joiner([], quoter=single, endcaps=brackets) == "[]"
+    assert joiner([1], quoter=single, endcaps=brackets) == "['1']"
+    assert joiner([1,2], quoter=single, endcaps=brackets) == "['1', '2']"
+    assert joiner([1,2,3], quoter=single, endcaps=brackets) == "['1', '2', '3']"
+
+def test_sep():
+    assert joiner([], sep='|') == ''
+    assert joiner([1], sep='|') == '1'
+    assert joiner([1,2], sep='|') == '1|2'
+    assert joiner([1,2,3], sep='|') == '1|2|3'
+
+def test_twosep_and_lastsep():
+    assert joiner([1,2,3,4], sep='|', lastsep='+') == '1|2|3+4'
+
+    assert joiner([], sep='|', twosep='*', lastsep='+') == ''
+    assert joiner([1], sep='|', twosep='*', lastsep='+') == '1'
+    assert joiner([1,2], sep='|', twosep='*', lastsep='+') == '1*2'
+    assert joiner([1,2,3], sep='|', twosep='*', lastsep='+') == '1|2+3'
+    assert joiner([1,2,3,4], sep='|', twosep='*', lastsep='+') == '1|2|3+4'
+
+def test_no_twostep():
+    assert joiner([], sep='|', twosep=None, lastsep='+') == ''
+    assert joiner([1], sep='|', twosep=None, lastsep='+') == '1'
+    assert joiner([1,2], sep='|', twosep=None, lastsep='+') == '1+2'
+    assert joiner([1,2,3], sep='|', twosep=None, lastsep='+') == '1|2+3'
+    assert joiner([1,2,3,4], sep='|', twosep=None, lastsep='+') == '1|2|3+4'
+
+
+def test_concat():
+    assert concat(4,5,6) == '456'
+    # assert concat(range(3)) == '012'
+    # assert concat('a','b','c') == 'abc'
+
+def test_and_join():
+    assert and_join([]) == ''
+    assert and_join([1]) == '1'
+    assert and_join([1,2]) == '1 and 2'
+    assert and_join([1,2,3]) == '1, 2, and 3'
+    assert and_join([1,2,3,4]) == '1, 2, 3, and 4'
+
+def test_or_join():
+    assert or_join([]) == ''
+    assert or_join([1]) == '1'
+    assert or_join([1,2]) == '1 or 2'
+    assert or_join([1,2,3]) == '1, 2, or 3'
+    assert or_join([1,2,3,4]) == '1, 2, 3, or 4'
+
+def test_items():
+    assert items([1,2,3,'string']) == "0: 1\n1: 2\n2: 3\n3: 'string'"
+
+    assert items([1,2,3,'string'], header="---") == "---\n0: 1\n1: 2\n2: 3\n3: 'string'"
+    assert items([1,2,3,'string'], footer="===") == "0: 1\n1: 2\n2: 3\n3: 'string'\n==="
+    assert items([1,2,3,'string'], header='---', footer="===") == "---\n0: 1\n1: 2\n2: 3\n3: 'string'\n==="
+
+
+    try:
+        from collections import OrderedDict
+        answer = "this: something\nthat: else\nplus: additionally"
+    except ImportError:
+        OrderedDict = dict # py26
+        answer = "this: something\nplus: additionally\nthat: else"
+        # output in different order because unordred dict
+    od = OrderedDict()
+    od['this'] = 'something'
+    od['that'] = 'else'
+    od['plus'] = 'additionally'
+
+    assert items(od, fmt="{key}: {value}") == answer
+
+    assert items(od, fmt="{key}: {value}", header="KEY: VALUE") == \
+        "KEY: VALUE\n" + answer

test/test_styling.py

     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_readme_example2():
+    fmt = Fmt()
+    name = 'Joe'
+    assert six.u('His name is ') + fmt(name, style='blue+underline') == six.u('His name is \x1b[34;4mJoe\x1b[0m')
+    assert fmt('His name is {name:style=blue+underline}') == six.u('His name is \x1b[34;4mJoe\x1b[0m')
+    assert fmt('His name is {name:style="blue+underline"}') == six.u('His name is \x1b[34;4mJoe\x1b[0m')
+    assert fmt("His name is {name:style='blue+underline'}") == six.u('His name is \x1b[34;4mJoe\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."
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.