Skip to content

Commit

Permalink
Migrate Jim Allman's nested styling enhancement
Browse files Browse the repository at this point in the history
@jimallman brings us this convenience of allowing existing
strings to be joined as a call parameter argument to a
FormattingString, allowing nestation:

This was rejected upstream as erikrose#45

```python
t.red('This is ', t.bold('extremely important'), ' information!')

t.green('foo', t.bold('bar', t.underline('baz'), 'herp'), 'derp')
```
  • Loading branch information
jquast committed Oct 10, 2015
1 parent ed420d5 commit d6f9159
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 33 deletions.
67 changes: 43 additions & 24 deletions blessed/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,18 @@ class ParameterizingProxyString(six.text_type):
given positional ``*args`` of :meth:`ParameterizingProxyString.__call__`
into a terminal sequence.
For example:
>>> from blessed import Terminal
>>> term = Terminal('screen')
>>> hpa = ParameterizingString(term.hpa, term.normal, 'hpa')
>>> hpa(9)
u''
>>> fmt = u'\x1b[{0}G'
>>> fmt_arg = lambda *arg: (arg[0] + 1,)
>>> hpa = ParameterizingProxyString((fmt, fmt_arg), term.normal, 'hpa')
>>> hpa(9)
u'\x1b[10G'
For example::
>>> from blessed import Terminal
>>> term = Terminal('screen')
>>> hpa = ParameterizingString(term.hpa, term.normal, 'hpa')
>>> hpa(9)
u''
>>> fmt = u'\x1b[{0}G'
>>> fmt_arg = lambda *arg: (arg[0] + 1,)
>>> hpa = ParameterizingProxyString((fmt, fmt_arg), term.normal, 'hpa')
>>> hpa(9)
u'\x1b[10G'
"""

def __new__(cls, *args):
Expand Down Expand Up @@ -208,13 +208,13 @@ class FormattingString(six.text_type):
directly, or as a callable. When used directly, it simply emits
the given terminal sequence. When used as a callable, it wraps the
given (string) argument with the 2nd argument used by the class
constructor.
constructor::
>>> style = FormattingString(term.bright_blue, term.normal)
>>> print(repr(style))
u'\x1b[94m'
>>> style('Big Blue')
u'\x1b[94mBig Blue\x1b(B\x1b[m'
>>> style = FormattingString(term.bright_blue, term.normal)
>>> print(repr(style))
u'\x1b[94m'
>>> style('Big Blue')
u'\x1b[94mBig Blue\x1b(B\x1b[m'
"""

def __new__(cls, *args):
Expand All @@ -229,11 +229,30 @@ def __new__(cls, *args):
new._normal = len(args) > 1 and args[1] or u''
return new

def __call__(self, text):
def __call__(self, *args):
"""Return ``text`` joined by ``sequence`` and ``normal``."""
if len(self):
return u''.join((self, text, self._normal))
return text
# Jim Allman brings us this convenience of allowing existing
# unicode strings to be joined as a call parameter to a formatting
# string result, allowing nestation:
#
# >>> t.red('This is ', t.bold('extremely'), ' dangerous!')
for idx, ucs_part in enumerate(args):
if not isinstance(ucs_part, six.string_types):
raise TypeError("Positional argument #{idx} is {is_type} "
"expected any of {expected_types}: "
"{ucs_part!r}".format(
idx=idx, ucs_part=ucs_part,
is_type=type(ucs_part),
expected_types=six.string_types,
))
postfix = u''
if len(self) and self._normal:
postfix = self._normal
_refresh = self._normal + self
args = [_refresh.join(ucs_part.split(self._normal))
for ucs_part in args]

return self + u''.join(args) + postfix


class NullCallableString(six.text_type):
Expand Down Expand Up @@ -262,7 +281,7 @@ def __call__(self, *args):
the first arg, acting in place of :class:`FormattingString` without
any attributes.
"""
if len(args) != 1 or isinstance(args[0], int):
if len(args) == 0 or isinstance(args[0], int):
# I am acting as a ParameterizingString.

# tparm can take not only ints but also (at least) strings as its
Expand All @@ -283,7 +302,7 @@ def __call__(self, *args):
# without color support, so turtles all the way down: we return
# another instance.
return NullCallableString()
return args[0]
return u''.join(args)


def split_compound(compound):
Expand Down
45 changes: 39 additions & 6 deletions blessed/tests/test_formatters.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# -*- coding: utf-8 -*-
"""Tests string formatting functions."""
# std
import curses

# 3rd-party
import mock
import pytest


def test_parameterizing_string_args_unspecified(monkeypatch):
Expand Down Expand Up @@ -70,7 +74,7 @@ def test_parameterizing_string_args(monkeypatch):


def test_parameterizing_string_type_error(monkeypatch):
"""Test formatters.ParameterizingString raising TypeError"""
"""Test formatters.ParameterizingString raising TypeError."""
from blessed.formatters import ParameterizingString

def tparm_raises_TypeError(*args):
Expand Down Expand Up @@ -106,7 +110,7 @@ def tparm_raises_TypeError(*args):


def test_formattingstring(monkeypatch):
"""Test formatters.FormattingString"""
"""Test simple __call__ behavior of formatters.FormattingString."""
from blessed.formatters import FormattingString

# given, with arg
Expand All @@ -117,23 +121,52 @@ def test_formattingstring(monkeypatch):
assert str(pstr) == u'attr'
assert pstr('text') == u'attrtextnorm'

# given, without arg
# given, with empty attribute
pstr = FormattingString(u'', u'norm')
assert pstr('text') == u'text'


def test_nested_formattingstring(monkeypatch):
"""Test nested __call__ behavior of formatters.FormattingString."""
from blessed.formatters import FormattingString

# given, with arg
pstr = FormattingString(u'a1-', u'n-')
zstr = FormattingString(u'a2-', u'n-')

# exercise __call__
assert pstr('x-', zstr('f-'), 'q-') == 'a1-x-a2-f-n-a1-q-n-'


def test_nested_formattingstring_type_error(monkeypatch):
"""Test formatters.FormattingString raising TypeError."""
from blessed.formatters import FormattingString

# given,
pstr = FormattingString(u'a-', u'n-')
expected_msg = (
"Positional argument #1 is {0} expected any of "
.format(type(1)))

# exercise,
with pytest.raises(TypeError) as err:
pstr('text', 1, '...')

# verify,
assert expected_msg in '{0}'.format(err)


def test_nullcallablestring(monkeypatch):
"""Test formatters.NullCallableString"""
from blessed.formatters import (NullCallableString)

# given, with arg
pstr = NullCallableString()

# excersize __call__,
# exercise __call__,
assert str(pstr) == u''
assert pstr('text') == u'text'
assert pstr('text', 1) == u''
assert pstr('text', 'moretext') == u''
assert pstr('text', 'moretext') == u'textmoretext'
assert pstr(99, 1) == u''
assert pstr() == u''
assert pstr(0) == u''
Expand Down
45 changes: 42 additions & 3 deletions blessed/tests/test_sequences.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,33 @@ def child(kind):
child(all_terms)


def test_nested_formatting(all_terms):
"""Test complex nested compound formatting, wow!"""
@as_subprocess
def child(kind):
t = TestTerminal(kind=kind)

# Test deeply nested styles
given = t.green('-a-', t.bold('-b-', t.underline('-c-'),
'-d-'),
'-e-')
expected = u''.join((
t.green, '-a-', t.bold, '-b-', t.underline, '-c-', t.normal,
t.green, t.bold, '-d-',
t.normal, t.green, '-e-', t.normal))
assert given == expected

# Test off-and-on nested styles
given = t.green('off ', t.underline('ON'),
' off ', t.underline('ON'),
' off')
expected = u''.join((
t.green, 'off ', t.underline, 'ON',
t.normal, t.green , ' off ', t.underline, 'ON',
t.normal, t.green, ' off', t.normal))
assert given == expected


def test_formatting_functions_without_tty(all_terms):
"""Test crazy-ass formatting wrappers when there's no tty."""
@as_subprocess
Expand All @@ -451,6 +478,20 @@ def child(kind):
# Test non-ASCII chars, no longer really necessary:
assert (t.bold_green(u'boö') == u'boö')
assert (t.bold_underline_green_on_red('loo') == u'loo')

# Test deeply nested styles
given = t.green('-a-', t.bold('-b-', t.underline('-c-'),
'-d-'),
'-e-')
expected = u'-a--b--c--d--e-'
assert given == expected

# Test off-and-on nested styles
given = t.green('off ', t.underline('ON'),
' off ', t.underline('ON'),
' off')
expected = u'off ON off ON off'
assert given == expected
assert (t.on_bright_red_bold_bright_green_underline('meh') == u'meh')

child(all_terms)
Expand Down Expand Up @@ -502,9 +543,7 @@ def child(kind):
assert (t.move(1 == 2) == '')
assert (t.move_x(1) == '')
assert (t.bold() == '')
assert (t.bold('', 'x', 'huh?') == '')
assert (t.bold('', 9876) == '')
assert (t.uhh(9876) == '')
assert (t.bold('', 'x', 'huh?') == 'xhuh?')
assert (t.clear('x') == 'x')

child(all_terms)
Expand Down
2 changes: 2 additions & 0 deletions docs/history.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Version History
* enhancement: :meth:`~.Terminal.inkey` can return more quickly for
combinations such as ``Alt + Z`` when ``MetaSendsEscape`` is enabled,
:ghissue:`30`.
* enhancement: :class:`~.FormattingString` may now be nested, such as
``t.red('red', t.underline('rum'))``, :ghissue:`61`

1.10
* workaround: provide ``sc`` and ``rc`` for Terminals of ``kind='ansi'``,
Expand Down

0 comments on commit d6f9159

Please sign in to comment.