diff options
Diffstat (limited to 'python/blessings')
-rw-r--r-- | python/blessings/LICENSE | 19 | ||||
-rw-r--r-- | python/blessings/MANIFEST.in | 3 | ||||
-rw-r--r-- | python/blessings/PKG-INFO | 426 | ||||
-rw-r--r-- | python/blessings/README.rst | 399 | ||||
-rw-r--r-- | python/blessings/blessings/__init__.py | 450 | ||||
-rw-r--r-- | python/blessings/blessings/tests.py | 231 | ||||
-rw-r--r-- | python/blessings/setup.cfg | 5 | ||||
-rw-r--r-- | python/blessings/setup.py | 42 | ||||
-rw-r--r-- | python/blessings/tox.ini | 7 |
9 files changed, 1582 insertions, 0 deletions
diff --git a/python/blessings/LICENSE b/python/blessings/LICENSE new file mode 100644 index 000000000..3d3a44e65 --- /dev/null +++ b/python/blessings/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011 Erik Rose + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/python/blessings/MANIFEST.in b/python/blessings/MANIFEST.in new file mode 100644 index 000000000..3f4fbd708 --- /dev/null +++ b/python/blessings/MANIFEST.in @@ -0,0 +1,3 @@ +include README.rst +include LICENSE +include tox.ini diff --git a/python/blessings/PKG-INFO b/python/blessings/PKG-INFO new file mode 100644 index 000000000..c52ca3cf9 --- /dev/null +++ b/python/blessings/PKG-INFO @@ -0,0 +1,426 @@ +Metadata-Version: 1.0 +Name: blessings +Version: 1.3 +Summary: A thin, practical wrapper around terminal formatting, positioning, and more +Home-page: https://github.com/erikrose/blessings +Author: Erik Rose +Author-email: erikrose@grinchcentral.com +License: MIT +Description: ========= + Blessings + ========= + + Coding with Blessings looks like this... :: + + from blessings import Terminal + + t = Terminal() + + print t.bold('Hi there!') + print t.bold_red_on_bright_green('It hurts my eyes!') + + with t.location(0, t.height - 1): + print 'This is at the bottom.' + + Or, for byte-level control, you can drop down and play with raw terminal + capabilities:: + + print '{t.bold}All your {t.red}bold and red base{t.normal}'.format(t=t) + print t.wingo(2) + + The Pitch + ========= + + Blessings lifts several of curses_' limiting assumptions, and it makes your + code pretty, too: + + * Use styles, color, and maybe a little positioning without clearing the whole + screen first. + * Leave more than one screenful of scrollback in the buffer after your program + exits, like a well-behaved command-line app should. + * Get rid of all those noisy, C-like calls to ``tigetstr`` and ``tparm``, so + your code doesn't get crowded out by terminal bookkeeping. + * Act intelligently when somebody redirects your output to a file, omitting the + terminal control codes the user doesn't want to see (optional). + + .. _curses: http://docs.python.org/library/curses.html + + Before And After + ---------------- + + Without Blessings, this is how you'd print some underlined text at the bottom + of the screen:: + + from curses import tigetstr, setupterm, tparm + from fcntl import ioctl + from os import isatty + import struct + import sys + from termios import TIOCGWINSZ + + # If we want to tolerate having our output piped to other commands or + # files without crashing, we need to do all this branching: + if hasattr(sys.stdout, 'fileno') and isatty(sys.stdout.fileno()): + setupterm() + sc = tigetstr('sc') + cup = tigetstr('cup') + rc = tigetstr('rc') + underline = tigetstr('smul') + normal = tigetstr('sgr0') + else: + sc = cup = rc = underline = normal = '' + print sc # Save cursor position. + if cup: + # tigetnum('lines') doesn't always update promptly, hence this: + height = struct.unpack('hhhh', ioctl(0, TIOCGWINSZ, '\000' * 8))[0] + print tparm(cup, height - 1, 0) # Move cursor to bottom. + print 'This is {under}underlined{normal}!'.format(under=underline, + normal=normal) + print rc # Restore cursor position. + + Phew! That was long and full of incomprehensible trash! Let's try it again, + this time with Blessings:: + + from blessings import Terminal + + term = Terminal() + with term.location(0, term.height - 1): + print 'This is', term.underline('pretty!') + + Much better. + + What It Provides + ================ + + Blessings provides just one top-level object: ``Terminal``. Instantiating a + ``Terminal`` figures out whether you're on a terminal at all and, if so, does + any necessary terminal setup. After that, you can proceed to ask it all sorts + of things about the terminal. Terminal terminal terminal. + + Simple Formatting + ----------------- + + Lots of handy formatting codes ("capabilities" in low-level parlance) are + available as attributes on a ``Terminal``. For example:: + + from blessings import Terminal + + term = Terminal() + print 'I am ' + term.bold + 'bold' + term.normal + '!' + + You can also use them as wrappers so you don't have to say ``normal`` + afterward:: + + print 'I am', term.bold('bold') + '!' + + Or, if you want fine-grained control while maintaining some semblance of + brevity, you can combine it with Python's string formatting, which makes + attributes easy to access:: + + print 'All your {t.red}base {t.underline}are belong to us{t.normal}'.format(t=term) + + Simple capabilities of interest include... + + * ``bold`` + * ``reverse`` + * ``underline`` + * ``no_underline`` (which turns off underlining) + * ``blink`` + * ``normal`` (which turns off everything, even colors) + * ``clear_eol`` (clear to the end of the line) + * ``clear_bol`` (clear to beginning of line) + * ``clear_eos`` (clear to end of screen) + + Here are a few more which are less likely to work on all terminals: + + * ``dim`` + * ``italic`` and ``no_italic`` + * ``shadow`` and ``no_shadow`` + * ``standout`` and ``no_standout`` + * ``subscript`` and ``no_subscript`` + * ``superscript`` and ``no_superscript`` + * ``flash`` (which flashes the screen once) + + Note that, while the inverse of ``underline`` is ``no_underline``, the only way + to turn off ``bold`` or ``reverse`` is ``normal``, which also cancels any + custom colors. This is because there's no way to tell the terminal to undo + certain pieces of formatting, even at the lowest level. + + You might notice that the above aren't the typical incomprehensible terminfo + capability names; we alias a few of the harder-to-remember ones for + readability. However, you aren't limited to these: you can reference any + string-returning capability listed on the `terminfo man page`_ by the name + under the "Cap-name" column: for example, ``term.rum``. + + .. _`terminfo man page`: http://www.manpagez.com/man/5/terminfo/ + + Color + ----- + + 16 colors, both foreground and background, are available as easy-to-remember + attributes:: + + from blessings import Terminal + + term = Terminal() + print term.red + term.on_green + 'Red on green? Ick!' + term.normal + print term.bright_red + term.on_bright_blue + 'This is even worse!' + term.normal + + You can also call them as wrappers, which sets everything back to normal at the + end:: + + print term.red_on_green('Red on green? Ick!') + print term.yellow('I can barely see it.') + + The available colors are... + + * ``black`` + * ``red`` + * ``green`` + * ``yellow`` + * ``blue`` + * ``magenta`` + * ``cyan`` + * ``white`` + + You can set the background color instead of the foreground by prepending + ``on_``, as in ``on_blue``. There is also a ``bright`` version of each color: + for example, ``on_bright_blue``. + + There is also a numerical interface to colors, which takes an integer from + 0-15:: + + term.color(5) + 'Hello' + term.normal + term.on_color(3) + 'Hello' + term.normal + + term.color(5)('Hello') + term.on_color(3)('Hello') + + If some color is unsupported (for instance, if only the normal colors are + available, not the bright ones), trying to use it will, on most terminals, have + no effect: the foreground and background colors will stay as they were. You can + get fancy and do different things depending on the supported colors by checking + `number_of_colors`_. + + .. _`number_of_colors`: http://packages.python.org/blessings/#blessings.Terminal.number_of_colors + + Compound Formatting + ------------------- + + If you want to do lots of crazy formatting all at once, you can just mash it + all together:: + + from blessings import Terminal + + term = Terminal() + print term.bold_underline_green_on_yellow + 'Woo' + term.normal + + Or you can use your newly coined attribute as a wrapper, which implicitly sets + everything back to normal afterward:: + + print term.bold_underline_green_on_yellow('Woo') + + This compound notation comes in handy if you want to allow users to customize + the formatting of your app: just have them pass in a format specifier like + "bold_green" on the command line, and do a quick ``getattr(term, + that_option)('Your text')`` when you do your formatting. + + I'd be remiss if I didn't credit couleur_, where I probably got the idea for + all this mashing. + + .. _couleur: http://pypi.python.org/pypi/couleur + + Parametrized Capabilities + ------------------------- + + Some capabilities take parameters. Rather than making you dig up ``tparm()`` + all the time, we simply make such capabilities into callable strings. You can + pass the parameters right in:: + + from blessings import Terminal + + term = Terminal() + print term.move(10, 1) + + Here are some of interest: + + ``move`` + Position the cursor elsewhere. Parameters are y coordinate, then x + coordinate. + ``move_x`` + Move the cursor to the given column. + ``move_y`` + Move the cursor to the given row. + + You can also reference any other string-returning capability listed on the + `terminfo man page`_ by its name under the "Cap-name" column. + + .. _`terminfo man page`: http://www.manpagez.com/man/5/terminfo/ + + Height and Width + ---------------- + + It's simple to get the height and width of the terminal, in characters:: + + from blessings import Terminal + + term = Terminal() + height = term.height + width = term.width + + These are newly updated each time you ask for them, so they're safe to use from + SIGWINCH handlers. + + Temporary Repositioning + ----------------------- + + Sometimes you need to flit to a certain location, print something, and then + return: for example, when updating a progress bar at the bottom of the screen. + ``Terminal`` provides a context manager for doing this concisely:: + + from blessings import Terminal + + term = Terminal() + with term.location(0, term.height - 1): + print 'Here is the bottom.' + print 'This is back where I came from.' + + Parameters to ``location()`` are ``x`` and then ``y``, but you can also pass + just one of them, leaving the other alone. For example... :: + + with term.location(y=10): + print 'We changed just the row.' + + If you want to reposition permanently, see ``move``, in an example above. + + Pipe Savvy + ---------- + + If your program isn't attached to a terminal, like if it's being piped to + another command or redirected to a file, all the capability attributes on + ``Terminal`` will return empty strings. You'll get a nice-looking file without + any formatting codes gumming up the works. + + If you want to override this--like if you anticipate your program being piped + through ``less -r``, which handles terminal escapes just fine--pass + ``force_styling=True`` to the ``Terminal`` constructor. + + In any case, there is an ``is_a_tty`` attribute on ``Terminal`` that lets you + see whether the attached stream seems to be a terminal. If it's false, you + might refrain from drawing progress bars and other frippery, since you're + apparently headed into a pipe:: + + from blessings import Terminal + + term = Terminal() + if term.is_a_tty: + with term.location(0, term.height - 1): + print 'Progress: [=======> ]' + print term.bold('Important stuff') + + Shopping List + ============= + + There are decades of legacy tied up in terminal interaction, so attention to + detail and behavior in edge cases make a difference. Here are some ways + Blessings has your back: + + * Uses the terminfo database so it works with any terminal type + * Provides up-to-the-moment terminal height and width, so you can respond to + terminal size changes (SIGWINCH signals). (Most other libraries query the + ``COLUMNS`` and ``LINES`` environment variables or the ``cols`` or ``lines`` + terminal capabilities, which don't update promptly, if at all.) + * Avoids making a mess if the output gets piped to a non-terminal + * Works great with standard Python string templating + * Provides convenient access to all terminal capabilities, not just a sugared + few + * Outputs to any file-like object, not just stdout + * Keeps a minimum of internal state, so you can feel free to mix and match with + calls to curses or whatever other terminal libraries you like + + Blessings does not provide... + + * Native color support on the Windows command prompt. However, it should work + when used in concert with colorama_. + + .. _colorama: http://pypi.python.org/pypi/colorama/0.2.4 + + Bugs + ==== + + Bugs or suggestions? Visit the `issue tracker`_. + + .. _`issue tracker`: https://github.com/erikrose/blessings/issues/new + + License + ======= + + Blessings is under the MIT License. See the LICENSE file. + + Version History + =============== + + 1.3 + * Add ``number_of_colors``, which tells you how many colors the terminal + supports. + * Made ``color(n)`` and ``on_color(n)`` callable to wrap a string, like the + named colors can. Also, make them both fall back to the ``setf`` and + ``setb`` capabilities (like the named colors do) if the ANSI ``setaf`` and + ``setab`` aren't available. + * Allow ``color`` attr to act as an unparametrized string, not just a + callable. + * Make ``height`` and ``width`` examine any passed-in stream before falling + back to stdout. (This rarely if ever affects actual behavior; it's mostly + philosophical.) + * Make caching simpler and slightly more efficient. + * Get rid of a reference cycle between Terminals and FormattingStrings. + * Update docs to reflect that terminal addressing (as in ``location()``) is + 0-based. + + 1.2 + * Added support for Python 3! We need 3.2.3 or greater, because the curses + library couldn't decide whether to accept strs or bytes before that + (http://bugs.python.org/issue10570). + * Everything that comes out of the library is now unicode. This lets us + support Python 3 without making a mess of the code, and Python 2 should + continue to work unless you were testing types (and badly). Please file a + bug if this causes trouble for you. + * Changed to the MIT License for better world domination. + * Added Sphinx docs. + + 1.1 + * Added nicely named attributes for colors. + * Introduced compound formatting. + * Added wrapper behavior for styling and colors. + * Let you force capabilities to be non-empty, even if the output stream is + not a terminal. + * Added the ``is_a_tty`` attribute for telling whether the output stream is a + terminal. + * Sugared the remaining interesting string capabilities. + * Let ``location()`` operate on just an x *or* y coordinate. + + 1.0 + * Extracted Blessings from nose-progressive, my `progress-bar-having, + traceback-shortcutting, rootin', tootin' testrunner`_. It provided the + tootin' functionality. + + .. _`progress-bar-having, traceback-shortcutting, rootin', tootin' testrunner`: http://pypi.python.org/pypi/nose-progressive/ + +Keywords: terminal,tty,curses,ncurses,formatting,style,color,console +Platform: UNKNOWN +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Environment :: Console :: Curses +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.5 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Software Development :: User Interfaces +Classifier: Topic :: Terminals diff --git a/python/blessings/README.rst b/python/blessings/README.rst new file mode 100644 index 000000000..59983de86 --- /dev/null +++ b/python/blessings/README.rst @@ -0,0 +1,399 @@ +========= +Blessings +========= + +Coding with Blessings looks like this... :: + + from blessings import Terminal + + t = Terminal() + + print t.bold('Hi there!') + print t.bold_red_on_bright_green('It hurts my eyes!') + + with t.location(0, t.height - 1): + print 'This is at the bottom.' + +Or, for byte-level control, you can drop down and play with raw terminal +capabilities:: + + print '{t.bold}All your {t.red}bold and red base{t.normal}'.format(t=t) + print t.wingo(2) + +The Pitch +========= + +Blessings lifts several of curses_' limiting assumptions, and it makes your +code pretty, too: + +* Use styles, color, and maybe a little positioning without clearing the whole + screen first. +* Leave more than one screenful of scrollback in the buffer after your program + exits, like a well-behaved command-line app should. +* Get rid of all those noisy, C-like calls to ``tigetstr`` and ``tparm``, so + your code doesn't get crowded out by terminal bookkeeping. +* Act intelligently when somebody redirects your output to a file, omitting the + terminal control codes the user doesn't want to see (optional). + +.. _curses: http://docs.python.org/library/curses.html + +Before And After +---------------- + +Without Blessings, this is how you'd print some underlined text at the bottom +of the screen:: + + from curses import tigetstr, setupterm, tparm + from fcntl import ioctl + from os import isatty + import struct + import sys + from termios import TIOCGWINSZ + + # If we want to tolerate having our output piped to other commands or + # files without crashing, we need to do all this branching: + if hasattr(sys.stdout, 'fileno') and isatty(sys.stdout.fileno()): + setupterm() + sc = tigetstr('sc') + cup = tigetstr('cup') + rc = tigetstr('rc') + underline = tigetstr('smul') + normal = tigetstr('sgr0') + else: + sc = cup = rc = underline = normal = '' + print sc # Save cursor position. + if cup: + # tigetnum('lines') doesn't always update promptly, hence this: + height = struct.unpack('hhhh', ioctl(0, TIOCGWINSZ, '\000' * 8))[0] + print tparm(cup, height - 1, 0) # Move cursor to bottom. + print 'This is {under}underlined{normal}!'.format(under=underline, + normal=normal) + print rc # Restore cursor position. + +Phew! That was long and full of incomprehensible trash! Let's try it again, +this time with Blessings:: + + from blessings import Terminal + + term = Terminal() + with term.location(0, term.height - 1): + print 'This is', term.underline('pretty!') + +Much better. + +What It Provides +================ + +Blessings provides just one top-level object: ``Terminal``. Instantiating a +``Terminal`` figures out whether you're on a terminal at all and, if so, does +any necessary terminal setup. After that, you can proceed to ask it all sorts +of things about the terminal. Terminal terminal terminal. + +Simple Formatting +----------------- + +Lots of handy formatting codes ("capabilities" in low-level parlance) are +available as attributes on a ``Terminal``. For example:: + + from blessings import Terminal + + term = Terminal() + print 'I am ' + term.bold + 'bold' + term.normal + '!' + +You can also use them as wrappers so you don't have to say ``normal`` +afterward:: + + print 'I am', term.bold('bold') + '!' + +Or, if you want fine-grained control while maintaining some semblance of +brevity, you can combine it with Python's string formatting, which makes +attributes easy to access:: + + print 'All your {t.red}base {t.underline}are belong to us{t.normal}'.format(t=term) + +Simple capabilities of interest include... + +* ``bold`` +* ``reverse`` +* ``underline`` +* ``no_underline`` (which turns off underlining) +* ``blink`` +* ``normal`` (which turns off everything, even colors) +* ``clear_eol`` (clear to the end of the line) +* ``clear_bol`` (clear to beginning of line) +* ``clear_eos`` (clear to end of screen) + +Here are a few more which are less likely to work on all terminals: + +* ``dim`` +* ``italic`` and ``no_italic`` +* ``shadow`` and ``no_shadow`` +* ``standout`` and ``no_standout`` +* ``subscript`` and ``no_subscript`` +* ``superscript`` and ``no_superscript`` +* ``flash`` (which flashes the screen once) + +Note that, while the inverse of ``underline`` is ``no_underline``, the only way +to turn off ``bold`` or ``reverse`` is ``normal``, which also cancels any +custom colors. This is because there's no way to tell the terminal to undo +certain pieces of formatting, even at the lowest level. + +You might notice that the above aren't the typical incomprehensible terminfo +capability names; we alias a few of the harder-to-remember ones for +readability. However, you aren't limited to these: you can reference any +string-returning capability listed on the `terminfo man page`_ by the name +under the "Cap-name" column: for example, ``term.rum``. + +.. _`terminfo man page`: http://www.manpagez.com/man/5/terminfo/ + +Color +----- + +16 colors, both foreground and background, are available as easy-to-remember +attributes:: + + from blessings import Terminal + + term = Terminal() + print term.red + term.on_green + 'Red on green? Ick!' + term.normal + print term.bright_red + term.on_bright_blue + 'This is even worse!' + term.normal + +You can also call them as wrappers, which sets everything back to normal at the +end:: + + print term.red_on_green('Red on green? Ick!') + print term.yellow('I can barely see it.') + +The available colors are... + +* ``black`` +* ``red`` +* ``green`` +* ``yellow`` +* ``blue`` +* ``magenta`` +* ``cyan`` +* ``white`` + +You can set the background color instead of the foreground by prepending +``on_``, as in ``on_blue``. There is also a ``bright`` version of each color: +for example, ``on_bright_blue``. + +There is also a numerical interface to colors, which takes an integer from +0-15:: + + term.color(5) + 'Hello' + term.normal + term.on_color(3) + 'Hello' + term.normal + + term.color(5)('Hello') + term.on_color(3)('Hello') + +If some color is unsupported (for instance, if only the normal colors are +available, not the bright ones), trying to use it will, on most terminals, have +no effect: the foreground and background colors will stay as they were. You can +get fancy and do different things depending on the supported colors by checking +`number_of_colors`_. + +.. _`number_of_colors`: http://packages.python.org/blessings/#blessings.Terminal.number_of_colors + +Compound Formatting +------------------- + +If you want to do lots of crazy formatting all at once, you can just mash it +all together:: + + from blessings import Terminal + + term = Terminal() + print term.bold_underline_green_on_yellow + 'Woo' + term.normal + +Or you can use your newly coined attribute as a wrapper, which implicitly sets +everything back to normal afterward:: + + print term.bold_underline_green_on_yellow('Woo') + +This compound notation comes in handy if you want to allow users to customize +the formatting of your app: just have them pass in a format specifier like +"bold_green" on the command line, and do a quick ``getattr(term, +that_option)('Your text')`` when you do your formatting. + +I'd be remiss if I didn't credit couleur_, where I probably got the idea for +all this mashing. + +.. _couleur: http://pypi.python.org/pypi/couleur + +Parametrized Capabilities +------------------------- + +Some capabilities take parameters. Rather than making you dig up ``tparm()`` +all the time, we simply make such capabilities into callable strings. You can +pass the parameters right in:: + + from blessings import Terminal + + term = Terminal() + print term.move(10, 1) + +Here are some of interest: + +``move`` + Position the cursor elsewhere. Parameters are y coordinate, then x + coordinate. +``move_x`` + Move the cursor to the given column. +``move_y`` + Move the cursor to the given row. + +You can also reference any other string-returning capability listed on the +`terminfo man page`_ by its name under the "Cap-name" column. + +.. _`terminfo man page`: http://www.manpagez.com/man/5/terminfo/ + +Height and Width +---------------- + +It's simple to get the height and width of the terminal, in characters:: + + from blessings import Terminal + + term = Terminal() + height = term.height + width = term.width + +These are newly updated each time you ask for them, so they're safe to use from +SIGWINCH handlers. + +Temporary Repositioning +----------------------- + +Sometimes you need to flit to a certain location, print something, and then +return: for example, when updating a progress bar at the bottom of the screen. +``Terminal`` provides a context manager for doing this concisely:: + + from blessings import Terminal + + term = Terminal() + with term.location(0, term.height - 1): + print 'Here is the bottom.' + print 'This is back where I came from.' + +Parameters to ``location()`` are ``x`` and then ``y``, but you can also pass +just one of them, leaving the other alone. For example... :: + + with term.location(y=10): + print 'We changed just the row.' + +If you want to reposition permanently, see ``move``, in an example above. + +Pipe Savvy +---------- + +If your program isn't attached to a terminal, like if it's being piped to +another command or redirected to a file, all the capability attributes on +``Terminal`` will return empty strings. You'll get a nice-looking file without +any formatting codes gumming up the works. + +If you want to override this--like if you anticipate your program being piped +through ``less -r``, which handles terminal escapes just fine--pass +``force_styling=True`` to the ``Terminal`` constructor. + +In any case, there is an ``is_a_tty`` attribute on ``Terminal`` that lets you +see whether the attached stream seems to be a terminal. If it's false, you +might refrain from drawing progress bars and other frippery, since you're +apparently headed into a pipe:: + + from blessings import Terminal + + term = Terminal() + if term.is_a_tty: + with term.location(0, term.height - 1): + print 'Progress: [=======> ]' + print term.bold('Important stuff') + +Shopping List +============= + +There are decades of legacy tied up in terminal interaction, so attention to +detail and behavior in edge cases make a difference. Here are some ways +Blessings has your back: + +* Uses the terminfo database so it works with any terminal type +* Provides up-to-the-moment terminal height and width, so you can respond to + terminal size changes (SIGWINCH signals). (Most other libraries query the + ``COLUMNS`` and ``LINES`` environment variables or the ``cols`` or ``lines`` + terminal capabilities, which don't update promptly, if at all.) +* Avoids making a mess if the output gets piped to a non-terminal +* Works great with standard Python string templating +* Provides convenient access to all terminal capabilities, not just a sugared + few +* Outputs to any file-like object, not just stdout +* Keeps a minimum of internal state, so you can feel free to mix and match with + calls to curses or whatever other terminal libraries you like + +Blessings does not provide... + +* Native color support on the Windows command prompt. However, it should work + when used in concert with colorama_. + +.. _colorama: http://pypi.python.org/pypi/colorama/0.2.4 + +Bugs +==== + +Bugs or suggestions? Visit the `issue tracker`_. + +.. _`issue tracker`: https://github.com/erikrose/blessings/issues/new + +License +======= + +Blessings is under the MIT License. See the LICENSE file. + +Version History +=============== + +1.3 + * Add ``number_of_colors``, which tells you how many colors the terminal + supports. + * Made ``color(n)`` and ``on_color(n)`` callable to wrap a string, like the + named colors can. Also, make them both fall back to the ``setf`` and + ``setb`` capabilities (like the named colors do) if the ANSI ``setaf`` and + ``setab`` aren't available. + * Allow ``color`` attr to act as an unparametrized string, not just a + callable. + * Make ``height`` and ``width`` examine any passed-in stream before falling + back to stdout. (This rarely if ever affects actual behavior; it's mostly + philosophical.) + * Make caching simpler and slightly more efficient. + * Get rid of a reference cycle between Terminals and FormattingStrings. + * Update docs to reflect that terminal addressing (as in ``location()``) is + 0-based. + +1.2 + * Added support for Python 3! We need 3.2.3 or greater, because the curses + library couldn't decide whether to accept strs or bytes before that + (http://bugs.python.org/issue10570). + * Everything that comes out of the library is now unicode. This lets us + support Python 3 without making a mess of the code, and Python 2 should + continue to work unless you were testing types (and badly). Please file a + bug if this causes trouble for you. + * Changed to the MIT License for better world domination. + * Added Sphinx docs. + +1.1 + * Added nicely named attributes for colors. + * Introduced compound formatting. + * Added wrapper behavior for styling and colors. + * Let you force capabilities to be non-empty, even if the output stream is + not a terminal. + * Added the ``is_a_tty`` attribute for telling whether the output stream is a + terminal. + * Sugared the remaining interesting string capabilities. + * Let ``location()`` operate on just an x *or* y coordinate. + +1.0 + * Extracted Blessings from nose-progressive, my `progress-bar-having, + traceback-shortcutting, rootin', tootin' testrunner`_. It provided the + tootin' functionality. + +.. _`progress-bar-having, traceback-shortcutting, rootin', tootin' testrunner`: http://pypi.python.org/pypi/nose-progressive/ diff --git a/python/blessings/blessings/__init__.py b/python/blessings/blessings/__init__.py new file mode 100644 index 000000000..081288ba6 --- /dev/null +++ b/python/blessings/blessings/__init__.py @@ -0,0 +1,450 @@ +from collections import defaultdict +import curses +from curses import tigetstr, tigetnum, setupterm, tparm +from fcntl import ioctl +try: + from io import UnsupportedOperation as IOUnsupportedOperation +except ImportError: + class IOUnsupportedOperation(Exception): + """A dummy exception to take the place of Python 3's ``io.UnsupportedOperation`` in Python 2""" + pass +import os +from os import isatty, environ +from platform import python_version_tuple +import struct +import sys +from termios import TIOCGWINSZ + + +if ('3', '0', '0') <= python_version_tuple() < ('3', '2', '2+'): # Good till 3.2.10 + # Python 3.x < 3.2.3 has a bug in which tparm() erroneously takes a string. + raise ImportError('Blessings needs Python 3.2.3 or greater for Python 3 ' + 'support due to http://bugs.python.org/issue10570.') + + +__all__ = ['Terminal'] + + +class Terminal(object): + """An abstraction around terminal capabilities + + Unlike curses, this doesn't require clearing the screen before doing + anything, and it's friendlier to use. It keeps the endless calls to + ``tigetstr()`` and ``tparm()`` out of your code, and it acts intelligently + when somebody pipes your output to a non-terminal. + + Instance attributes: + + ``stream`` + The stream the terminal outputs to. It's convenient to pass the stream + around with the terminal; it's almost always needed when the terminal + is and saves sticking lots of extra args on client functions in + practice. + ``is_a_tty`` + Whether ``stream`` appears to be a terminal. You can examine this value + to decide whether to draw progress bars or other frippery. + + """ + def __init__(self, kind=None, stream=None, force_styling=False): + """Initialize the terminal. + + If ``stream`` is not a tty, I will default to returning an empty + Unicode string for all capability values, so things like piping your + output to a file won't strew escape sequences all over the place. The + ``ls`` command sets a precedent for this: it defaults to columnar + output when being sent to a tty and one-item-per-line when not. + + :arg kind: A terminal string as taken by ``setupterm()``. Defaults to + the value of the ``TERM`` environment variable. + :arg stream: A file-like object representing the terminal. Defaults to + the original value of stdout, like ``curses.initscr()`` does. + :arg force_styling: Whether to force the emission of capabilities, even + if we don't seem to be in a terminal. This comes in handy if users + are trying to pipe your output through something like ``less -r``, + which supports terminal codes just fine but doesn't appear itself + to be a terminal. Just expose a command-line option, and set + ``force_styling`` based on it. Terminal initialization sequences + will be sent to ``stream`` if it has a file descriptor and to + ``sys.__stdout__`` otherwise. (``setupterm()`` demands to send them + somewhere, and stdout is probably where the output is ultimately + headed. If not, stderr is probably bound to the same terminal.) + + """ + if stream is None: + stream = sys.__stdout__ + try: + stream_descriptor = (stream.fileno() if hasattr(stream, 'fileno') + and callable(stream.fileno) + else None) + except IOUnsupportedOperation: + stream_descriptor = None + + self.is_a_tty = stream_descriptor is not None and isatty(stream_descriptor) + self._does_styling = self.is_a_tty or force_styling + + # The desciptor to direct terminal initialization sequences to. + # sys.__stdout__ seems to always have a descriptor of 1, even if output + # is redirected. + self._init_descriptor = (sys.__stdout__.fileno() + if stream_descriptor is None + else stream_descriptor) + if self._does_styling: + # Make things like tigetstr() work. Explicit args make setupterm() + # work even when -s is passed to nosetests. Lean toward sending + # init sequences to the stream if it has a file descriptor, and + # send them to stdout as a fallback, since they have to go + # somewhere. + setupterm(kind or environ.get('TERM', 'unknown'), + self._init_descriptor) + + self.stream = stream + + # Sugary names for commonly-used capabilities, intended to help avoid trips + # to the terminfo man page and comments in your code: + _sugar = dict( + # Don't use "on" or "bright" as an underscore-separated chunk in any of + # these (e.g. on_cology or rock_on) so we don't interfere with + # __getattr__. + save='sc', + restore='rc', + + clear_eol='el', + clear_bol='el1', + clear_eos='ed', + position='cup', # deprecated + move='cup', + move_x='hpa', + move_y='vpa', + + reset_colors='op', # oc doesn't work on my OS X terminal. + + normal='sgr0', + reverse='rev', + # 'bold' is just 'bold'. Similarly... + # blink + # dim + # flash + italic='sitm', + no_italic='ritm', + shadow='sshm', + no_shadow='rshm', + standout='smso', + no_standout='rmso', + subscript='ssubm', + no_subscript='rsubm', + superscript='ssupm', + no_superscript='rsupm', + underline='smul', + no_underline='rmul') + + def __getattr__(self, attr): + """Return parametrized terminal capabilities, like bold. + + For example, you can say ``term.bold`` to get the string that turns on + bold formatting and ``term.normal`` to get the string that turns it off + again. Or you can take a shortcut: ``term.bold('hi')`` bolds its + argument and sets everything to normal afterward. You can even combine + things: ``term.bold_underline_red_on_bright_green('yowzers!')``. + + For a parametrized capability like ``cup``, pass the parameters too: + ``some_term.cup(line, column)``. + + ``man terminfo`` for a complete list of capabilities. + + Return values are always Unicode. + + """ + resolution = self._resolve_formatter(attr) if self._does_styling else NullCallableString() + setattr(self, attr, resolution) # Cache capability codes. + return resolution + + @property + def height(self): + """The height of the terminal in characters + + If no stream or a stream not representing a terminal was passed in at + construction, return the dimension of the controlling terminal so + piping to things that eventually display on the terminal (like ``less + -R``) work. If a stream representing a terminal was passed in, return + the dimensions of that terminal. If there somehow is no controlling + terminal, return ``None``. (Thus, you should check that ``is_a_tty`` is + true before doing any math on the result.) + + """ + return self._height_and_width()[0] + + @property + def width(self): + """The width of the terminal in characters + + See ``height()`` for some corner cases. + + """ + return self._height_and_width()[1] + + def _height_and_width(self): + """Return a tuple of (terminal height, terminal width).""" + # tigetnum('lines') and tigetnum('cols') update only if we call + # setupterm() again. + for descriptor in self._init_descriptor, sys.__stdout__: + try: + return struct.unpack('hhhh', ioctl(descriptor, TIOCGWINSZ, '\000' * 8))[0:2] + except IOError: + pass + return None, None # Should never get here + + def location(self, x=None, y=None): + """Return a context manager for temporarily moving the cursor. + + Move the cursor to a certain position on entry, let you print stuff + there, then return the cursor to its original position:: + + term = Terminal() + with term.location(2, 5): + print 'Hello, world!' + for x in xrange(10): + print 'I can do it %i times!' % x + + Specify ``x`` to move to a certain column, ``y`` to move to a certain + row, or both. + + """ + return Location(self, x, y) + + @property + def color(self): + """Return a capability that sets the foreground color. + + The capability is unparametrized until called and passed a number + (0-15), at which point it returns another string which represents a + specific color change. This second string can further be called to + color a piece of text and set everything back to normal afterward. + + :arg num: The number, 0-15, of the color + + """ + return ParametrizingString(self._foreground_color, self.normal) + + @property + def on_color(self): + """Return a capability that sets the background color. + + See ``color()``. + + """ + return ParametrizingString(self._background_color, self.normal) + + @property + def number_of_colors(self): + """Return the number of colors the terminal supports. + + Common values are 0, 8, 16, 88, and 256. + + Though the underlying capability returns -1 when there is no color + support, we return 0. This lets you test more Pythonically:: + + if term.number_of_colors: + ... + + We also return 0 if the terminal won't tell us how many colors it + supports, which I think is rare. + + """ + # This is actually the only remotely useful numeric capability. We + # don't name it after the underlying capability, because we deviate + # slightly from its behavior, and we might someday wish to give direct + # access to it. + colors = tigetnum('colors') # Returns -1 if no color support, -2 if no such cap. + #self.__dict__['colors'] = ret # Cache it. It's not changing. (Doesn't work.) + return colors if colors >= 0 else 0 + + def _resolve_formatter(self, attr): + """Resolve a sugary or plain capability name, color, or compound formatting function name into a callable capability.""" + if attr in COLORS: + return self._resolve_color(attr) + elif attr in COMPOUNDABLES: + # Bold, underline, or something that takes no parameters + return self._formatting_string(self._resolve_capability(attr)) + else: + formatters = split_into_formatters(attr) + if all(f in COMPOUNDABLES for f in formatters): + # It's a compound formatter, like "bold_green_on_red". Future + # optimization: combine all formatting into a single escape + # sequence. + return self._formatting_string( + u''.join(self._resolve_formatter(s) for s in formatters)) + else: + return ParametrizingString(self._resolve_capability(attr)) + + def _resolve_capability(self, atom): + """Return a terminal code for a capname or a sugary name, or an empty Unicode. + + The return value is always Unicode, because otherwise it is clumsy + (especially in Python 3) to concatenate with real (Unicode) strings. + + """ + code = tigetstr(self._sugar.get(atom, atom)) + if code: + # We can encode escape sequences as UTF-8 because they never + # contain chars > 127, and UTF-8 never changes anything within that + # range.. + return code.decode('utf-8') + return u'' + + def _resolve_color(self, color): + """Resolve a color like red or on_bright_green into a callable capability.""" + # TODO: Does curses automatically exchange red and blue and cyan and + # yellow when a terminal supports setf/setb rather than setaf/setab? + # I'll be blasted if I can find any documentation. The following + # assumes it does. + color_cap = (self._background_color if 'on_' in color else + self._foreground_color) + # curses constants go up to only 7, so add an offset to get at the + # bright colors at 8-15: + offset = 8 if 'bright_' in color else 0 + base_color = color.rsplit('_', 1)[-1] + return self._formatting_string( + color_cap(getattr(curses, 'COLOR_' + base_color.upper()) + offset)) + + @property + def _foreground_color(self): + return self.setaf or self.setf + + @property + def _background_color(self): + return self.setab or self.setb + + def _formatting_string(self, formatting): + """Return a new ``FormattingString`` which implicitly receives my notion of "normal".""" + return FormattingString(formatting, self.normal) + + +def derivative_colors(colors): + """Return the names of valid color variants, given the base colors.""" + return set([('on_' + c) for c in colors] + + [('bright_' + c) for c in colors] + + [('on_bright_' + c) for c in colors]) + + +COLORS = set(['black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white']) +COLORS.update(derivative_colors(COLORS)) +COMPOUNDABLES = (COLORS | + set(['bold', 'underline', 'reverse', 'blink', 'dim', 'italic', + 'shadow', 'standout', 'subscript', 'superscript'])) + + +class ParametrizingString(unicode): + """A Unicode string which can be called to parametrize it as a terminal capability""" + def __new__(cls, formatting, normal=None): + """Instantiate. + + :arg normal: If non-None, indicates that, once parametrized, this can + be used as a ``FormattingString``. The value is used as the + "normal" capability. + + """ + new = unicode.__new__(cls, formatting) + new._normal = normal + return new + + def __call__(self, *args): + try: + # Re-encode the cap, because tparm() takes a bytestring in Python + # 3. However, appear to be a plain Unicode string otherwise so + # concats work. + parametrized = tparm(self.encode('utf-8'), *args).decode('utf-8') + return (parametrized if self._normal is None else + FormattingString(parametrized, self._normal)) + except curses.error: + # Catch "must call (at least) setupterm() first" errors, as when + # running simply `nosetests` (without progressive) on nose- + # progressive. Perhaps the terminal has gone away between calling + # tigetstr and calling tparm. + return u'' + except TypeError: + # If the first non-int (i.e. incorrect) arg was a string, suggest + # something intelligent: + if len(args) == 1 and isinstance(args[0], basestring): + raise TypeError( + 'A native or nonexistent capability template received ' + '%r when it was expecting ints. You probably misspelled a ' + 'formatting call like bright_red_on_white(...).' % args) + else: + # Somebody passed a non-string; I don't feel confident + # guessing what they were trying to do. + raise + + +class FormattingString(unicode): + """A Unicode string which can be called upon a piece of text to wrap it in formatting""" + def __new__(cls, formatting, normal): + new = unicode.__new__(cls, formatting) + new._normal = normal + return new + + def __call__(self, text): + """Return a new string that is ``text`` formatted with my contents. + + At the beginning of the string, I prepend the formatting that is my + contents. At the end, I append the "normal" sequence to set everything + back to defaults. The return value is always a Unicode. + + """ + return self + text + self._normal + + +class NullCallableString(unicode): + """A dummy class to stand in for ``FormattingString`` and ``ParametrizingString`` + + A callable bytestring that returns an empty Unicode when called with an int + and the arg otherwise. We use this when there is no tty and so all + capabilities are blank. + + """ + def __new__(cls): + new = unicode.__new__(cls, u'') + return new + + def __call__(self, arg): + if isinstance(arg, int): + return u'' + return arg # TODO: Force even strs in Python 2.x to be unicodes? Nah. How would I know what encoding to use to convert it? + + +def split_into_formatters(compound): + """Split a possibly compound format string into segments. + + >>> split_into_formatters('bold_underline_bright_blue_on_red') + ['bold', 'underline', 'bright_blue', 'on_red'] + + """ + merged_segs = [] + # These occur only as prefixes, so they can always be merged: + mergeable_prefixes = ['on', 'bright', 'on_bright'] + for s in compound.split('_'): + if merged_segs and merged_segs[-1] in mergeable_prefixes: + merged_segs[-1] += '_' + s + else: + merged_segs.append(s) + return merged_segs + + +class Location(object): + """Context manager for temporarily moving the cursor""" + def __init__(self, term, x=None, y=None): + self.x, self.y = x, y + self.term = term + + def __enter__(self): + """Save position and move to the requested column, row, or both.""" + self.term.stream.write(self.term.save) # save position + if self.x and self.y: + self.term.stream.write(self.term.move(self.y, self.x)) + elif self.x: + self.term.stream.write(self.term.move_x(self.x)) + elif self.y: + self.term.stream.write(self.term.move_y(self.y)) + + def __exit__(self, type, value, tb): + """Restore original cursor position.""" + self.term.stream.write(self.term.restore) diff --git a/python/blessings/blessings/tests.py b/python/blessings/blessings/tests.py new file mode 100644 index 000000000..a02a3924a --- /dev/null +++ b/python/blessings/blessings/tests.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +"""Automated tests (as opposed to human-verified test patterns) + +It was tempting to mock out curses to get predictable output from ``tigetstr``, +but there are concrete integration-testing benefits in not doing so. For +instance, ``tigetstr`` changed its return type in Python 3.2.3. So instead, we +simply create all our test ``Terminal`` instances with a known terminal type. +All we require from the host machine is that a standard terminfo definition of +xterm-256color exists. + +""" +from __future__ import with_statement # Make 2.5-compatible +from curses import tigetstr, tparm +from functools import partial +from StringIO import StringIO +import sys + +from nose import SkipTest +from nose.tools import eq_ + +# This tests that __all__ is correct, since we use below everything that should +# be imported: +from blessings import * + + +TestTerminal = partial(Terminal, kind='xterm-256color') + + +def unicode_cap(cap): + """Return the result of ``tigetstr`` except as Unicode.""" + return tigetstr(cap).decode('utf-8') + + +def unicode_parm(cap, *parms): + """Return the result of ``tparm(tigetstr())`` except as Unicode.""" + return tparm(tigetstr(cap), *parms).decode('utf-8') + + +def test_capability(): + """Check that a capability lookup works. + + Also test that Terminal grabs a reasonable default stream. This test + assumes it will be run from a tty. + + """ + t = TestTerminal() + sc = unicode_cap('sc') + eq_(t.save, sc) + eq_(t.save, sc) # Make sure caching doesn't screw it up. + + +def test_capability_without_tty(): + """Assert capability templates are '' when stream is not a tty.""" + t = TestTerminal(stream=StringIO()) + eq_(t.save, u'') + eq_(t.red, u'') + + +def test_capability_with_forced_tty(): + """If we force styling, capabilities had better not (generally) be empty.""" + t = TestTerminal(stream=StringIO(), force_styling=True) + eq_(t.save, unicode_cap('sc')) + + +def test_parametrization(): + """Test parametrizing a capability.""" + eq_(TestTerminal().cup(3, 4), unicode_parm('cup', 3, 4)) + + +def height_and_width(): + """Assert that ``height_and_width()`` returns ints.""" + t = TestTerminal() # kind shouldn't matter. + assert isinstance(int, t.height) + assert isinstance(int, t.width) + + +def test_stream_attr(): + """Make sure Terminal exposes a ``stream`` attribute that defaults to something sane.""" + eq_(Terminal().stream, sys.__stdout__) + + +def test_location(): + """Make sure ``location()`` does what it claims.""" + t = TestTerminal(stream=StringIO(), force_styling=True) + + with t.location(3, 4): + t.stream.write(u'hi') + + eq_(t.stream.getvalue(), unicode_cap('sc') + + unicode_parm('cup', 4, 3) + + u'hi' + + unicode_cap('rc')) + + +def test_horizontal_location(): + """Make sure we can move the cursor horizontally without changing rows.""" + t = TestTerminal(stream=StringIO(), force_styling=True) + with t.location(x=5): + pass + eq_(t.stream.getvalue(), unicode_cap('sc') + + unicode_parm('hpa', 5) + + unicode_cap('rc')) + + +def test_null_fileno(): + """Make sure ``Terminal`` works when ``fileno`` is ``None``. + + This simulates piping output to another program. + + """ + out = StringIO() + out.fileno = None + t = TestTerminal(stream=out) + eq_(t.save, u'') + + +def test_mnemonic_colors(): + """Make sure color shortcuts work.""" + def color(num): + return unicode_parm('setaf', num) + + def on_color(num): + return unicode_parm('setab', num) + + # Avoid testing red, blue, yellow, and cyan, since they might someday + # change depending on terminal type. + t = TestTerminal() + eq_(t.white, color(7)) + eq_(t.green, color(2)) # Make sure it's different than white. + eq_(t.on_black, on_color(0)) + eq_(t.on_green, on_color(2)) + eq_(t.bright_black, color(8)) + eq_(t.bright_green, color(10)) + eq_(t.on_bright_black, on_color(8)) + eq_(t.on_bright_green, on_color(10)) + + +def test_callable_numeric_colors(): + """``color(n)`` should return a formatting wrapper.""" + t = TestTerminal() + eq_(t.color(5)('smoo'), t.magenta + 'smoo' + t.normal) + eq_(t.color(5)('smoo'), t.color(5) + 'smoo' + t.normal) + eq_(t.on_color(2)('smoo'), t.on_green + 'smoo' + t.normal) + eq_(t.on_color(2)('smoo'), t.on_color(2) + 'smoo' + t.normal) + + +def test_null_callable_numeric_colors(): + """``color(n)`` should be a no-op on null terminals.""" + t = TestTerminal(stream=StringIO()) + eq_(t.color(5)('smoo'), 'smoo') + eq_(t.on_color(6)('smoo'), 'smoo') + + +def test_naked_color_cap(): + """``term.color`` should return a stringlike capability.""" + t = TestTerminal() + eq_(t.color + '', t.setaf + '') + + +def test_number_of_colors_without_tty(): + """``number_of_colors`` should return 0 when there's no tty.""" + # Hypothesis: once setupterm() has run and decided the tty supports 256 + # colors, it never changes its mind. + raise SkipTest + + t = TestTerminal(stream=StringIO()) + eq_(t.number_of_colors, 0) + t = TestTerminal(stream=StringIO(), force_styling=True) + eq_(t.number_of_colors, 0) + + +def test_number_of_colors_with_tty(): + """``number_of_colors`` should work.""" + t = TestTerminal() + eq_(t.number_of_colors, 256) + + +def test_formatting_functions(): + """Test crazy-ass formatting wrappers, both simple and compound.""" + t = TestTerminal() + # By now, it should be safe to use sugared attributes. Other tests test those. + eq_(t.bold(u'hi'), t.bold + u'hi' + t.normal) + eq_(t.green('hi'), t.green + u'hi' + t.normal) # Plain strs for Python 2.x + # Test some non-ASCII chars, probably not necessary: + eq_(t.bold_green(u'boö'), t.bold + t.green + u'boö' + t.normal) + eq_(t.bold_underline_green_on_red('boo'), + t.bold + t.underline + t.green + t.on_red + u'boo' + t.normal) + # Don't spell things like this: + eq_(t.on_bright_red_bold_bright_green_underline('meh'), + t.on_bright_red + t.bold + t.bright_green + t.underline + u'meh' + t.normal) + + +def test_formatting_functions_without_tty(): + """Test crazy-ass formatting wrappers when there's no tty.""" + t = TestTerminal(stream=StringIO()) + eq_(t.bold(u'hi'), u'hi') + eq_(t.green('hi'), u'hi') + # Test non-ASCII chars, no longer really necessary: + eq_(t.bold_green(u'boö'), u'boö') + eq_(t.bold_underline_green_on_red('loo'), u'loo') + eq_(t.on_bright_red_bold_bright_green_underline('meh'), u'meh') + + +def test_nice_formatting_errors(): + """Make sure you get nice hints if you misspell a formatting wrapper.""" + t = TestTerminal() + try: + t.bold_misspelled('hey') + except TypeError, e: + assert 'probably misspelled' in e.args[0] + + try: + t.bold_misspelled(u'hey') # unicode + except TypeError, e: + assert 'probably misspelled' in e.args[0] + + try: + t.bold_misspelled(None) # an arbitrary non-string + except TypeError, e: + assert 'probably misspelled' not in e.args[0] + + try: + t.bold_misspelled('a', 'b') # >1 string arg + except TypeError, e: + assert 'probably misspelled' not in e.args[0] + + +def test_init_descriptor_always_initted(): + """We should be able to get a height and width even on no-tty Terminals.""" + t = Terminal(stream=StringIO()) + eq_(type(t.height), int) diff --git a/python/blessings/setup.cfg b/python/blessings/setup.cfg new file mode 100644 index 000000000..861a9f554 --- /dev/null +++ b/python/blessings/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/python/blessings/setup.py b/python/blessings/setup.py new file mode 100644 index 000000000..6af55452d --- /dev/null +++ b/python/blessings/setup.py @@ -0,0 +1,42 @@ +import sys + +from setuptools import setup, find_packages + + +extra_setup = {} +if sys.version_info >= (3,): + extra_setup['use_2to3'] = True + +setup( + name='blessings', + version='1.3', + description='A thin, practical wrapper around terminal formatting, positioning, and more', + long_description=open('README.rst').read(), + author='Erik Rose', + author_email='erikrose@grinchcentral.com', + license='MIT', + packages=find_packages(exclude=['ez_setup']), + tests_require=['Nose'], + url='https://github.com/erikrose/blessings', + include_package_data=True, + classifiers=[ + 'Intended Audience :: Developers', + 'Natural Language :: English', + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Environment :: Console :: Curses', + 'License :: OSI Approved :: MIT License', + 'Operating System :: POSIX', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: User Interfaces', + 'Topic :: Terminals' + ], + keywords=['terminal', 'tty', 'curses', 'ncurses', 'formatting', 'style', 'color', 'console'], + **extra_setup +) diff --git a/python/blessings/tox.ini b/python/blessings/tox.ini new file mode 100644 index 000000000..e1753f261 --- /dev/null +++ b/python/blessings/tox.ini @@ -0,0 +1,7 @@ +[tox] +envlist = py25, py26, py27, py32 + +[testenv] +commands = nosetests blessings +deps = nose +changedir = .tox # So Python 3 runs don't pick up incompatible, un-2to3'd source from the cwd |