From d8fcd032acf53ee39076b59ab5fff79a8f8ca187 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Sun, 20 Nov 2011 21:14:09 -0800 Subject: Make some hopeful metadata changes and dodge a scoping change in Python 3. --- blessings/__init__.py | 14 +++++++++----- fabfile.py | 2 ++ setup.py | 8 ++++++++ tox.ini | 4 ++-- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/blessings/__init__.py b/blessings/__init__.py index dae686f..be71d0b 100644 --- a/blessings/__init__.py +++ b/blessings/__init__.py @@ -80,7 +80,7 @@ class Terminal(object): # Cache capability codes, because IIRC tigetstr requires a # conversation with the terminal. [Now I can't find any evidence of - # that.] + # that.] At any rate, save redoing the work of _resolve_formatter(). self._codes = {} else: self._codes = NullDict(lambda: NullCallableString('')) @@ -218,11 +218,15 @@ class Terminal(object): self) +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(set([('on_' + c) for c in COLORS] + - [('bright_' + c) for c in COLORS] + - [('on_bright_' + c) for c in COLORS])) -del c +COLORS.update(derivative_colors(COLORS)) COMPOUNDABLES = (COLORS | set(['bold', 'underline', 'reverse', 'blink', 'dim', 'italic', 'shadow', 'standout', 'subscript', 'superscript'])) diff --git a/fabfile.py b/fabfile.py index cc556bc..ebeeb6f 100644 --- a/fabfile.py +++ b/fabfile.py @@ -27,10 +27,12 @@ def doc(kind='html'): with cd('docs'): local('make clean %s' % kind) + def test(): # Just calling nosetests results in SUPPORTS_TRANSACTIONS KeyErrors. local('nosetests') + def updoc(): """Build Sphinx docs and upload them to packages.python.org. diff --git a/setup.py b/setup.py index bc0d3f4..b4f4c88 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,15 @@ setup( 'Intended Audience :: Developers', 'Natural Language :: English', 'Environment :: Console', + 'Environment :: Console :: Curses', '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.1', + 'Programming Language :: Python :: 3.2', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: User Interfaces', 'Topic :: Terminals' diff --git a/tox.ini b/tox.ini index 3204750..66b1c22 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] -envlist = py25, py26, py27 +envlist = py25, py26, py27, py32 [testenv] commands = nosetests -deps=nose \ No newline at end of file +deps=nose -- cgit v1.2.1 From 23d42b6ef30408a7558f565e247be452d68aeb35 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Sun, 20 Nov 2011 21:29:39 -0800 Subject: TypeError isn't subscriptable in Python 3. --- blessings/tests.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blessings/tests.py b/blessings/tests.py index d9e7d85..62de8fd 100644 --- a/blessings/tests.py +++ b/blessings/tests.py @@ -130,14 +130,14 @@ def test_nice_formatting_errors(): try: t.bold_misspelled('hey') except TypeError, e: - assert 'probably misspelled' in e[0] + 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[0] + 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[0] + assert 'probably misspelled' not in e.args[0] -- cgit v1.2.1 From 20c5f21c24509d10bd35b8f87a67651d91f3df4c Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Sun, 20 Nov 2011 22:07:13 -0800 Subject: fileno() exists on files all the time in Python 3, even if there is no file number. If there is none, it throws an exception when called. --- blessings/__init__.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/blessings/__init__.py b/blessings/__init__.py index be71d0b..333d68e 100644 --- a/blessings/__init__.py +++ b/blessings/__init__.py @@ -2,6 +2,12 @@ from collections import defaultdict import curses from curses import tigetstr, 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 one in Python 2""" + pass from os import isatty, environ import struct import sys @@ -59,9 +65,12 @@ class Terminal(object): """ if stream is None: stream = sys.__stdout__ - stream_descriptor = (stream.fileno() if hasattr(stream, 'fileno') - and callable(stream.fileno) - else None) + 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) if self.is_a_tty or force_styling: # The desciptor to direct terminal initialization sequences to. -- cgit v1.2.1 From d9aaa14bce0a1a16d0758c6f858594d6037e1d3f Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Mon, 21 Nov 2011 14:57:48 -0800 Subject: Make all encoding explicit, and blow only raw strings to the terminal. Tests pass in 2.6. --- README.rst | 5 +++ blessings/__init__.py | 93 +++++++++++++++++++++++++++++++++++++++------------ blessings/tests.py | 43 ++++++++++++++---------- 3 files changed, 103 insertions(+), 38 deletions(-) diff --git a/README.rst b/README.rst index c5aabd7..e95e02a 100644 --- a/README.rst +++ b/README.rst @@ -325,6 +325,11 @@ Bugs or suggestions? Visit the `issue tracker`_. Version History =============== +1.2 + * 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). + 1.1 * Added nicely named attributes for colors. * Introduced compound formatting. diff --git a/blessings/__init__.py b/blessings/__init__.py index 333d68e..6a099ef 100644 --- a/blessings/__init__.py +++ b/blessings/__init__.py @@ -8,12 +8,20 @@ except ImportError: class IOUnsupportedOperation(Exception): """A dummy exception to take the place of Python 3's io.UnsupportedOperation one in Python 2""" pass +import locale +import os from os import isatty, environ import struct import sys from termios import TIOCGWINSZ +try: + Capability = bytes +except NameError: + Capability = str # Python 2.5 + + __all__ = ['Terminal'] __version__ = (1, 1) @@ -38,7 +46,7 @@ class Terminal(object): to decide whether to draw progress bars or other frippery. """ - def __init__(self, kind=None, stream=None, force_styling=False): + def __init__(self, kind=None, stream=None, encoding=None, force_styling=False): """Initialize the terminal. If ``stream`` is not a tty, I will default to returning '' for all @@ -51,6 +59,10 @@ class Terminal(object): 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 encoding: The encoding to run any Unicode strings through before + they're output to the terminal. Terminals take bitstrings, so we've + got to pick something. We'll try a pretty fancy cascade of defaults + stolen from the standard ``io`` lib if you don't specify. :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``, @@ -71,6 +83,10 @@ class Terminal(object): else None) except IOUnsupportedOperation: stream_descriptor = None + + self.encoding = (self._guess_encoding(stream_descriptor) + if encoding is None else encoding) + self.is_a_tty = stream_descriptor is not None and isatty(stream_descriptor) if self.is_a_tty or force_styling: # The desciptor to direct terminal initialization sequences to. @@ -92,10 +108,29 @@ class Terminal(object): # that.] At any rate, save redoing the work of _resolve_formatter(). self._codes = {} else: - self._codes = NullDict(lambda: NullCallableString('')) + self._codes = NullDict(lambda: NullCap('', self.encoding)) self.stream = stream + def _guess_encoding(self, stream_descriptor): + """Return the guessed encoding of the terminal. + + We adapt the algorithm from io.TextIOWrapper, which is what ``print`` + natively calls in Python 3. + + """ + encoding = None + try: + if stream_descriptor is not None: + encoding = os.device_encoding(stream_descriptor) + except (AttributeError, IOUnsupportedOperation): + pass + if encoding is None: + # getpreferredencoding returns '' in OS X's "Western (NextStep)" + # terminal type. + encoding = locale.getpreferredencoding() or 'ascii' + return encoding + # Sugary names for commonly-used capabilities, intended to help avoid trips # to the terminfo man page and comments in your code: _sugar = dict( @@ -188,30 +223,30 @@ class Terminal(object): return Location(self, x, y) def _resolve_formatter(self, attr): - """Resolve a sugary or plain capability name, color, or compound formatting function name into a callable string.""" + """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 FormattingString(self._resolve_capability(attr), self) + return FormattingCap(self._resolve_capability(attr), self) 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 FormattingString(''.join(self._resolve_formatter(s) - for s in formatters), - self) + return FormattingCap(''.join(self._resolve_formatter(s) + for s in formatters), + self) else: - return ParametrizingString(self._resolve_capability(attr)) + return ParametrizingCap(self._resolve_capability(attr)) def _resolve_capability(self, atom): """Return a terminal code for a capname or a sugary name, or ''.""" return tigetstr(self._sugar.get(atom, atom)) or '' def _resolve_color(self, color): - """Resolve a color like red or on_bright_green into a callable string.""" + """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 @@ -222,7 +257,7 @@ class Terminal(object): # bright colors at 8-15: offset = 8 if 'bright_' in color else 0 base_color = color.rsplit('_', 1)[-1] - return FormattingString( + return FormattingCap( color_cap(getattr(curses, 'COLOR_' + base_color.upper()) + offset), self) @@ -241,8 +276,8 @@ COMPOUNDABLES = (COLORS | 'shadow', 'standout', 'subscript', 'superscript'])) -class ParametrizingString(str): - """A string which can be called to parametrize it as a terminal capability""" +class ParametrizingCap(Capability): + """A bytestring which can be called to parametrize it as a terminal capability""" def __call__(self, *args): try: return tparm(self, *args) @@ -251,7 +286,7 @@ class ParametrizingString(str): # running simply `nosetests` (without progressive) on nose- # progressive. Perhaps the terminal has gone away between calling # tigetstr and calling tparm. - return '' + return Capability() except TypeError: # If the first non-int (i.e. incorrect) arg was a string, suggest # something intelligent: @@ -266,11 +301,11 @@ class ParametrizingString(str): raise -class FormattingString(str): - """A string which can be called upon a piece of text to wrap it in formatting""" +class FormattingCap(Capability): + """A bytestring which can be called upon a piece of text to wrap it in formatting""" def __new__(cls, formatting, term): - new = str.__new__(cls, formatting) - new._term = term + new = Capability.__new__(cls, formatting) + new._term = term # TODO: Kill cycle. return new def __call__(self, text): @@ -283,15 +318,31 @@ class FormattingString(str): This should work regardless of whether ``text`` is unicode. """ + if isinstance(text, unicode): + text = text.encode(self._term.encoding) return self + text + self._term.normal -class NullCallableString(str): - """A callable string that returns '' when called with an int and the arg otherwise.""" +class NullCap(Capability): + """A dummy class to stand in for ``FormattingCap`` and ``ParametrizingCap`` + + A callable bytestring that returns ``''`` when called with an int and the + arg otherwise. We use this when tehre is no tty and so all capabilities are + blank. + + """ + def __new__(cls, cap, encoding): + new = Capability.__new__(cls, cap) + new._encoding = encoding + return new + def __call__(self, arg): if isinstance(arg, int): - return '' - return arg + return Capability() + elif isinstance(arg, unicode): + return arg.encode(self._encoding) + else: + return arg class NullDict(defaultdict): diff --git a/blessings/tests.py b/blessings/tests.py index 62de8fd..d8b5fcd 100644 --- a/blessings/tests.py +++ b/blessings/tests.py @@ -10,6 +10,12 @@ from nose.tools import eq_ # This tests that __all__ is correct, since we use below everything that should # be imported: from blessings import * +from blessings import Capability + + +def bytes_eq(bytes1, bytes2): + """Make sure ``bytes1`` equals ``bytes2``, the latter of which gets cast to something bytes-like, depending on Python version.""" + eq_(bytes1, Capability(bytes2)) def test_capability(): @@ -28,13 +34,14 @@ def test_capability(): def test_capability_without_tty(): """Assert capability templates are '' when stream is not a tty.""" t = Terminal(stream=StringIO()) - eq_(t.save, '') - eq_(t.red, '') + eq_(t.save, Capability('')) + eq_(t.red, Capability('')) def test_capability_with_forced_tty(): + """If we force styling, capabilities had better not (generally) be empty.""" t = Terminal(stream=StringIO(), force_styling=True) - assert t.save != '' + assert len(t.save) > 0 def test_parametrization(): @@ -63,7 +70,7 @@ def test_location(): eq_(t.stream.getvalue(), tigetstr('sc') + tparm(tigetstr('cup'), 4, 3) + - 'hi' + + 'hi' + # TODO: Encode with Terminal's encoding. tigetstr('rc')) def test_horizontal_location(): @@ -75,7 +82,7 @@ def test_horizontal_location(): def test_null_fileno(): - """Make sure ``Terinal`` works when ``fileno`` is ``None``. + """Make sure ``Terminal`` works when ``fileno`` is ``None``. This simulates piping output to another program. @@ -83,7 +90,7 @@ def test_null_fileno(): out = stream=StringIO() out.fileno = None t = Terminal(stream=out) - eq_(t.save, '') + bytes_eq(t.save, '') def test_mnemonic_colors(): @@ -91,22 +98,23 @@ def test_mnemonic_colors(): # Avoid testing red, blue, yellow, and cyan, since they might someday # chance depending on terminal type. t = Terminal() - eq_(t.white, '\x1b[37m') - eq_(t.green, '\x1b[32m') # Make sure it's different than white. - eq_(t.on_black, '\x1b[40m') - eq_(t.on_green, '\x1b[42m') - eq_(t.bright_black, '\x1b[90m') - eq_(t.bright_green, '\x1b[92m') - eq_(t.on_bright_black, '\x1b[100m') - eq_(t.on_bright_green, '\x1b[102m') + bytes_eq(t.white, '\x1b[37m') + bytes_eq(t.green, '\x1b[32m') # Make sure it's different than white. + bytes_eq(t.on_black, '\x1b[40m') + bytes_eq(t.on_green, '\x1b[42m') + bytes_eq(t.bright_black, '\x1b[90m') + bytes_eq(t.bright_green, '\x1b[92m') + bytes_eq(t.on_bright_black, '\x1b[100m') + bytes_eq(t.on_bright_green, '\x1b[102m') def test_formatting_functions(): """Test crazy-ass formatting wrappers, both simple and compound.""" - t = Terminal() + t = Terminal(encoding='utf-8') eq_(t.bold('hi'), t.bold + 'hi' + t.normal) eq_(t.green('hi'), t.green + 'hi' + t.normal) - eq_(t.bold_green(u'boö'), t.bold + t.green + u'boö' + t.normal) # unicode + # Test encoding of unicodes: + eq_(t.bold_green(u'boö'), t.bold + t.green + u'boö'.encode('utf-8') + t.normal) eq_(t.bold_underline_green_on_red('boo'), t.bold + t.underline + t.green + t.on_red + 'boo' + t.normal) # Don't spell things like this: @@ -119,7 +127,8 @@ def test_formatting_functions_without_tty(): t = Terminal(stream=StringIO()) eq_(t.bold('hi'), 'hi') eq_(t.green('hi'), 'hi') - eq_(t.bold_green(u'boö'), u'boö') # unicode + # Test encoding of unicodes: + eq_(t.bold_green(u'boö'), u'boö'.encode('utf-8')) # unicode eq_(t.bold_underline_green_on_red('boo'), 'boo') eq_(t.on_bright_red_bold_bright_green_underline('meh'), 'meh') -- cgit v1.2.1 From c1589b3aab168d68a169750b9f378a59665d5ff5 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Tue, 22 Nov 2011 14:50:10 -0800 Subject: Get the remaining tests passing in Python 3. Mostly just dealt with unicode issues. Still ugly in some places. --- blessings/__init__.py | 18 +++++++------- blessings/tests.py | 69 +++++++++++++++++++++++++++++---------------------- 2 files changed, 48 insertions(+), 39 deletions(-) diff --git a/blessings/__init__.py b/blessings/__init__.py index 6a099ef..7cbfd19 100644 --- a/blessings/__init__.py +++ b/blessings/__init__.py @@ -108,7 +108,7 @@ class Terminal(object): # that.] At any rate, save redoing the work of _resolve_formatter(). self._codes = {} else: - self._codes = NullDict(lambda: NullCap('', self.encoding)) + self._codes = NullDict(lambda: NullCap(self.encoding)) self.stream = stream @@ -234,16 +234,16 @@ class Terminal(object): 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 FormattingCap(''.join(self._resolve_formatter(s) - for s in formatters), - self) + # sequence. + return FormattingCap( + Capability().join(self._resolve_formatter(s) for s in formatters), + self) else: return ParametrizingCap(self._resolve_capability(attr)) def _resolve_capability(self, atom): """Return a terminal code for a capname or a sugary name, or ''.""" - return tigetstr(self._sugar.get(atom, atom)) or '' + return tigetstr(self._sugar.get(atom, atom)) or Capability() def _resolve_color(self, color): """Resolve a color like red or on_bright_green into a callable capability.""" @@ -327,12 +327,12 @@ class NullCap(Capability): """A dummy class to stand in for ``FormattingCap`` and ``ParametrizingCap`` A callable bytestring that returns ``''`` when called with an int and the - arg otherwise. We use this when tehre is no tty and so all capabilities are + arg otherwise. We use this when there is no tty and so all capabilities are blank. """ - def __new__(cls, cap, encoding): - new = Capability.__new__(cls, cap) + def __new__(cls, encoding): + new = Capability.__new__(cls, Capability()) new._encoding = encoding return new diff --git a/blessings/tests.py b/blessings/tests.py index d8b5fcd..5fcfb18 100644 --- a/blessings/tests.py +++ b/blessings/tests.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- from __future__ import with_statement # Make 2.5-compatible -from StringIO import StringIO +try: + from io import BytesIO +except ImportError: + from StringIO import StringIO as BytesIO from curses import tigetstr, tparm import sys @@ -33,14 +36,14 @@ def test_capability(): def test_capability_without_tty(): """Assert capability templates are '' when stream is not a tty.""" - t = Terminal(stream=StringIO()) - eq_(t.save, Capability('')) - eq_(t.red, Capability('')) + t = Terminal(stream=BytesIO()) + eq_(t.save, Capability(''.encode('utf-8'))) + eq_(t.red, Capability(''.encode('utf-8'))) def test_capability_with_forced_tty(): """If we force styling, capabilities had better not (generally) be empty.""" - t = Terminal(stream=StringIO(), force_styling=True) + t = Terminal(stream=BytesIO(), force_styling=True) assert len(t.save) > 0 @@ -63,19 +66,19 @@ def test_stream_attr(): def test_location(): """Make sure ``location()`` does what it claims.""" - t = Terminal(stream=StringIO(), force_styling=True) + t = Terminal(stream=BytesIO(), force_styling=True) with t.location(3, 4): - t.stream.write('hi') + t.stream.write('hi'.encode(t.encoding)) eq_(t.stream.getvalue(), tigetstr('sc') + tparm(tigetstr('cup'), 4, 3) + - 'hi' + # TODO: Encode with Terminal's encoding. + 'hi'.encode(t.encoding) + tigetstr('rc')) def test_horizontal_location(): """Make sure we can move the cursor horizontally without changing rows.""" - t = Terminal(stream=StringIO(), force_styling=True) + t = Terminal(stream=BytesIO(), force_styling=True) with t.location(x=5): pass eq_(t.stream.getvalue(), t.save + tparm(tigetstr('hpa'), 5) + t.restore) @@ -87,50 +90,56 @@ def test_null_fileno(): This simulates piping output to another program. """ - out = stream=StringIO() + out = stream=BytesIO() out.fileno = None t = Terminal(stream=out) - bytes_eq(t.save, '') + eq_(t.save, ''.encode('utf-8')) def test_mnemonic_colors(): """Make sure color shortcuts work.""" + def color(num): + return tparm(tigetstr('setaf'), num) + + def on_color(num): + return tparm(tigetstr('setab'), num) + # Avoid testing red, blue, yellow, and cyan, since they might someday # chance depending on terminal type. t = Terminal() - bytes_eq(t.white, '\x1b[37m') - bytes_eq(t.green, '\x1b[32m') # Make sure it's different than white. - bytes_eq(t.on_black, '\x1b[40m') - bytes_eq(t.on_green, '\x1b[42m') - bytes_eq(t.bright_black, '\x1b[90m') - bytes_eq(t.bright_green, '\x1b[92m') - bytes_eq(t.on_bright_black, '\x1b[100m') - bytes_eq(t.on_bright_green, '\x1b[102m') + eq_(t.white, color(7)) + bytes_eq(t.green, color(2)) # Make sure it's different than white. + bytes_eq(t.on_black, on_color(0)) + bytes_eq(t.on_green, on_color(2)) + bytes_eq(t.bright_black, color(8)) + bytes_eq(t.bright_green, color(10)) + bytes_eq(t.on_bright_black, on_color(8)) + bytes_eq(t.on_bright_green, on_color(10)) def test_formatting_functions(): """Test crazy-ass formatting wrappers, both simple and compound.""" t = Terminal(encoding='utf-8') - eq_(t.bold('hi'), t.bold + 'hi' + t.normal) - eq_(t.green('hi'), t.green + 'hi' + t.normal) + eq_(t.bold('hi'), t.bold + 'hi'.encode('utf-8') + t.normal) + eq_(t.green('hi'), t.green + 'hi'.encode('utf-8') + t.normal) # Test encoding of unicodes: - eq_(t.bold_green(u'boö'), t.bold + t.green + u'boö'.encode('utf-8') + t.normal) + eq_(t.bold_green('boö'), t.bold + t.green + 'boö'.encode('utf-8') + t.normal) eq_(t.bold_underline_green_on_red('boo'), - t.bold + t.underline + t.green + t.on_red + 'boo' + t.normal) + t.bold + t.underline + t.green + t.on_red + 'boo'.encode('utf-8') + 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 + 'meh' + t.normal) + t.on_bright_red + t.bold + t.bright_green + t.underline + 'meh'.encode('utf-8') + t.normal) def test_formatting_functions_without_tty(): """Test crazy-ass formatting wrappers when there's no tty.""" - t = Terminal(stream=StringIO()) - eq_(t.bold('hi'), 'hi') - eq_(t.green('hi'), 'hi') + t = Terminal(stream=BytesIO()) + eq_(t.bold('hi'), 'hi'.encode('utf-8')) + eq_(t.green('hi'), 'hi'.encode('utf-8')) # Test encoding of unicodes: - eq_(t.bold_green(u'boö'), u'boö'.encode('utf-8')) # unicode - eq_(t.bold_underline_green_on_red('boo'), 'boo') - eq_(t.on_bright_red_bold_bright_green_underline('meh'), 'meh') + eq_(t.bold_green('boö'), 'boö'.encode('utf-8')) # unicode + eq_(t.bold_underline_green_on_red('boo'), 'boo'.encode('utf-8')) + eq_(t.on_bright_red_bold_bright_green_underline('meh'), 'meh'.encode('utf-8')) def test_nice_formatting_errors(): -- cgit v1.2.1 From 5a4bcd4a84ac104d3926f26fe6beb1ebb374732e Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Tue, 22 Nov 2011 14:51:18 -0800 Subject: We need Python 3.2.3 or better, to fix a curses bug. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index b4f4c88..4450589 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,6 @@ setup( 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.1', 'Programming Language :: Python :: 3.2', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: User Interfaces', -- cgit v1.2.1 From 841dcf71ecb1acf84844a0d18929b1f75acdca91 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Tue, 22 Nov 2011 15:02:22 -0800 Subject: Re-unicode-ify some test literals. --- blessings/tests.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessings/tests.py b/blessings/tests.py index 5fcfb18..365a9f8 100644 --- a/blessings/tests.py +++ b/blessings/tests.py @@ -123,7 +123,7 @@ def test_formatting_functions(): eq_(t.bold('hi'), t.bold + 'hi'.encode('utf-8') + t.normal) eq_(t.green('hi'), t.green + 'hi'.encode('utf-8') + t.normal) # Test encoding of unicodes: - eq_(t.bold_green('boö'), t.bold + t.green + 'boö'.encode('utf-8') + t.normal) + eq_(t.bold_green(u'boö'), t.bold + t.green + u'boö'.encode('utf-8') + t.normal) eq_(t.bold_underline_green_on_red('boo'), t.bold + t.underline + t.green + t.on_red + 'boo'.encode('utf-8') + t.normal) # Don't spell things like this: @@ -137,7 +137,7 @@ def test_formatting_functions_without_tty(): eq_(t.bold('hi'), 'hi'.encode('utf-8')) eq_(t.green('hi'), 'hi'.encode('utf-8')) # Test encoding of unicodes: - eq_(t.bold_green('boö'), 'boö'.encode('utf-8')) # unicode + eq_(t.bold_green(u'boö'), u'boö'.encode('utf-8')) # unicode eq_(t.bold_underline_green_on_red('boo'), 'boo'.encode('utf-8')) eq_(t.on_bright_red_bold_bright_green_underline('meh'), 'meh'.encode('utf-8')) -- cgit v1.2.1 From 876f68033fdfcff9dcd4aa13f6a5a29ea24e044f Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Tue, 22 Nov 2011 18:57:52 -0800 Subject: Rip all that encoding stuff out. Terminal now outputs unicode, which Python 3's stdout TextIOWrapper is much happier to pass to the tty unaltered. Seems to work in 2.x. Unknown about 3.x. Tests don't pass at all and need to be rewritten. --- blessings/__init__.py | 100 +++++++++++++++++++------------------------------- 1 file changed, 38 insertions(+), 62 deletions(-) diff --git a/blessings/__init__.py b/blessings/__init__.py index 7cbfd19..f5dc107 100644 --- a/blessings/__init__.py +++ b/blessings/__init__.py @@ -16,12 +16,6 @@ import sys from termios import TIOCGWINSZ -try: - Capability = bytes -except NameError: - Capability = str # Python 2.5 - - __all__ = ['Terminal'] __version__ = (1, 1) @@ -46,23 +40,19 @@ class Terminal(object): to decide whether to draw progress bars or other frippery. """ - def __init__(self, kind=None, stream=None, encoding=None, force_styling=False): + def __init__(self, kind=None, stream=None, force_styling=False): """Initialize the terminal. - If ``stream`` is not a tty, I will default to returning '' for all + If ``stream`` is not a tty, I will default to returning ``u''`` 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. + 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 encoding: The encoding to run any Unicode strings through before - they're output to the terminal. Terminals take bitstrings, so we've - got to pick something. We'll try a pretty fancy cascade of defaults - stolen from the standard ``io`` lib if you don't specify. :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``, @@ -84,9 +74,6 @@ class Terminal(object): except IOUnsupportedOperation: stream_descriptor = None - self.encoding = (self._guess_encoding(stream_descriptor) - if encoding is None else encoding) - self.is_a_tty = stream_descriptor is not None and isatty(stream_descriptor) if self.is_a_tty or force_styling: # The desciptor to direct terminal initialization sequences to. @@ -108,29 +95,10 @@ class Terminal(object): # that.] At any rate, save redoing the work of _resolve_formatter(). self._codes = {} else: - self._codes = NullDict(lambda: NullCap(self.encoding)) + self._codes = NullDict(lambda: NullCap()) self.stream = stream - def _guess_encoding(self, stream_descriptor): - """Return the guessed encoding of the terminal. - - We adapt the algorithm from io.TextIOWrapper, which is what ``print`` - natively calls in Python 3. - - """ - encoding = None - try: - if stream_descriptor is not None: - encoding = os.device_encoding(stream_descriptor) - except (AttributeError, IOUnsupportedOperation): - pass - if encoding is None: - # getpreferredencoding returns '' in OS X's "Western (NextStep)" - # terminal type. - encoding = locale.getpreferredencoding() or 'ascii' - return encoding - # Sugary names for commonly-used capabilities, intended to help avoid trips # to the terminfo man page and comments in your code: _sugar = dict( @@ -187,6 +155,8 @@ class Terminal(object): ``man terminfo`` for a complete list of capabilities. + Return values are always Unicode. + """ if attr not in self._codes: # Store sugary names under the sugary keys to save a hash lookup. @@ -236,14 +206,25 @@ class Terminal(object): # optimization: combine all formatting into a single escape # sequence. return FormattingCap( - Capability().join(self._resolve_formatter(s) for s in formatters), + u''.join(self._resolve_formatter(s) for s in formatters), self) else: return ParametrizingCap(self._resolve_capability(attr)) def _resolve_capability(self, atom): - """Return a terminal code for a capname or a sugary name, or ''.""" - return tigetstr(self._sugar.get(atom, atom)) or Capability() + """Return a terminal code for a capname or a sugary name, or u''. + + 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.""" @@ -276,17 +257,20 @@ COMPOUNDABLES = (COLORS | 'shadow', 'standout', 'subscript', 'superscript'])) -class ParametrizingCap(Capability): - """A bytestring which can be called to parametrize it as a terminal capability""" +class ParametrizingCap(unicode): + """A Unicode string which can be called to parametrize it as a terminal capability""" def __call__(self, *args): try: - return tparm(self, *args) + # Re-encode the cap, because tparm() takes a bytestring in Python + # 3. However, appear to be a plain Unicode string otherwise so + # concats work. + return tparm(self.encode('utf-8'), *args).decode('utf-8') 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 Capability() + return u'' except TypeError: # If the first non-int (i.e. incorrect) arg was a string, suggest # something intelligent: @@ -301,10 +285,10 @@ class ParametrizingCap(Capability): raise -class FormattingCap(Capability): - """A bytestring which can be called upon a piece of text to wrap it in formatting""" +class FormattingCap(unicode): + """A Unicode string which can be called upon a piece of text to wrap it in formatting""" def __new__(cls, formatting, term): - new = Capability.__new__(cls, formatting) + new = unicode.__new__(cls, formatting) new._term = term # TODO: Kill cycle. return new @@ -315,34 +299,26 @@ class FormattingCap(Capability): contents. At the end, I append the "normal" sequence to set everything back to defaults. - This should work regardless of whether ``text`` is unicode. - """ - if isinstance(text, unicode): - text = text.encode(self._term.encoding) return self + text + self._term.normal -class NullCap(Capability): +class NullCap(unicode): """A dummy class to stand in for ``FormattingCap`` and ``ParametrizingCap`` - A callable bytestring that returns ``''`` when called with an int and the - arg otherwise. We use this when there is no tty and so all capabilities are - blank. + 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, encoding): - new = Capability.__new__(cls, Capability()) - new._encoding = encoding + def __new__(cls, dummy): + new = unicode.__new__(cls, u'') return new def __call__(self, arg): if isinstance(arg, int): - return Capability() - elif isinstance(arg, unicode): - return arg.encode(self._encoding) - else: - return arg + return u'' + return arg # TODO: Force even strs in Python 2.x to be unicodes? class NullDict(defaultdict): -- cgit v1.2.1 From 49c4dc46731068424698f912b9f3b76f4112bf82 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Tue, 22 Nov 2011 22:57:41 -0800 Subject: I have always hated the name "Capability" for these classes. Change it back to "String". --- blessings/__init__.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/blessings/__init__.py b/blessings/__init__.py index f5dc107..5082d1b 100644 --- a/blessings/__init__.py +++ b/blessings/__init__.py @@ -95,7 +95,7 @@ class Terminal(object): # that.] At any rate, save redoing the work of _resolve_formatter(). self._codes = {} else: - self._codes = NullDict(lambda: NullCap()) + self._codes = NullDict(lambda: NullCallableString()) self.stream = stream @@ -198,18 +198,18 @@ class Terminal(object): return self._resolve_color(attr) elif attr in COMPOUNDABLES: # Bold, underline, or something that takes no parameters - return FormattingCap(self._resolve_capability(attr), self) + return FormattingString(self._resolve_capability(attr), self) 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 FormattingCap( + return FormattingString( u''.join(self._resolve_formatter(s) for s in formatters), self) else: - return ParametrizingCap(self._resolve_capability(attr)) + return ParametrizingString(self._resolve_capability(attr)) def _resolve_capability(self, atom): """Return a terminal code for a capname or a sugary name, or u''. @@ -238,7 +238,7 @@ class Terminal(object): # bright colors at 8-15: offset = 8 if 'bright_' in color else 0 base_color = color.rsplit('_', 1)[-1] - return FormattingCap( + return FormattingString( color_cap(getattr(curses, 'COLOR_' + base_color.upper()) + offset), self) @@ -257,7 +257,7 @@ COMPOUNDABLES = (COLORS | 'shadow', 'standout', 'subscript', 'superscript'])) -class ParametrizingCap(unicode): +class ParametrizingString(unicode): """A Unicode string which can be called to parametrize it as a terminal capability""" def __call__(self, *args): try: @@ -285,7 +285,7 @@ class ParametrizingCap(unicode): raise -class FormattingCap(unicode): +class FormattingString(unicode): """A Unicode string which can be called upon a piece of text to wrap it in formatting""" def __new__(cls, formatting, term): new = unicode.__new__(cls, formatting) @@ -297,14 +297,14 @@ class FormattingCap(unicode): 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. + back to defaults. The return value is always a Unicode. """ return self + text + self._term.normal -class NullCap(unicode): - """A dummy class to stand in for ``FormattingCap`` and ``ParametrizingCap`` +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 @@ -318,7 +318,7 @@ class NullCap(unicode): def __call__(self, arg): if isinstance(arg, int): return u'' - return arg # TODO: Force even strs in Python 2.x to be unicodes? + 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? class NullDict(defaultdict): -- cgit v1.2.1 From af90fa34f16341620e95695da64aac0bf04cbec2 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Thu, 24 Nov 2011 18:50:22 -0800 Subject: Rethink the whole test suite. Passes in Python 2.6. Closes #6. --- blessings/__init__.py | 2 +- blessings/tests.py | 131 +++++++++++++++++++++++++++++--------------------- 2 files changed, 78 insertions(+), 55 deletions(-) diff --git a/blessings/__init__.py b/blessings/__init__.py index 5082d1b..e83fb5c 100644 --- a/blessings/__init__.py +++ b/blessings/__init__.py @@ -311,7 +311,7 @@ class NullCallableString(unicode): capabilities are blank. """ - def __new__(cls, dummy): + def __new__(cls): new = unicode.__new__(cls, u'') return new diff --git a/blessings/tests.py b/blessings/tests.py index 365a9f8..8690f4d 100644 --- a/blessings/tests.py +++ b/blessings/tests.py @@ -1,11 +1,18 @@ # -*- 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 -try: - from io import BytesIO -except ImportError: - from StringIO import StringIO as BytesIO from curses import tigetstr, tparm +from functools import partial +from StringIO import StringIO import sys from nose.tools import eq_ @@ -13,12 +20,19 @@ from nose.tools import eq_ # This tests that __all__ is correct, since we use below everything that should # be imported: from blessings import * -from blessings import Capability -def bytes_eq(bytes1, bytes2): - """Make sure ``bytes1`` equals ``bytes2``, the latter of which gets cast to something bytes-like, depending on Python version.""" - eq_(bytes1, Capability(bytes2)) +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(): @@ -28,33 +42,33 @@ def test_capability(): assumes it will be run from a tty. """ - t = Terminal() - sc = tigetstr('sc') + 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 = Terminal(stream=BytesIO()) - eq_(t.save, Capability(''.encode('utf-8'))) - eq_(t.red, Capability(''.encode('utf-8'))) + 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 = Terminal(stream=BytesIO(), force_styling=True) - assert len(t.save) > 0 + t = TestTerminal(stream=StringIO(), force_styling=True) + eq_(t.save, unicode_cap('sc')) def test_parametrization(): """Test parametrizing a capability.""" - eq_(Terminal().cup(3, 4), tparm(tigetstr('cup'), 3, 4)) + eq_(TestTerminal().cup(3, 4), unicode_parm('cup', 3, 4)) def height_and_width(): """Assert that ``height_and_width()`` returns ints.""" - t = Terminal() + t = TestTerminal() # kind shouldn't matter. assert isinstance(int, t.height) assert isinstance(int, t.width) @@ -66,22 +80,25 @@ def test_stream_attr(): def test_location(): """Make sure ``location()`` does what it claims.""" - t = Terminal(stream=BytesIO(), force_styling=True) + t = TestTerminal(stream=StringIO(), force_styling=True) with t.location(3, 4): - t.stream.write('hi'.encode(t.encoding)) + t.stream.write(u'hi') + + eq_(t.stream.getvalue(), unicode_cap('sc') + + unicode_parm('cup', 4, 3) + + u'hi' + + unicode_cap('rc')) - eq_(t.stream.getvalue(), tigetstr('sc') + - tparm(tigetstr('cup'), 4, 3) + - 'hi'.encode(t.encoding) + - tigetstr('rc')) def test_horizontal_location(): """Make sure we can move the cursor horizontally without changing rows.""" - t = Terminal(stream=BytesIO(), force_styling=True) + t = TestTerminal(stream=StringIO(), force_styling=True) with t.location(x=5): pass - eq_(t.stream.getvalue(), t.save + tparm(tigetstr('hpa'), 5) + t.restore) + eq_(t.stream.getvalue(), unicode_cap('sc') + + unicode_parm('hpa', 5) + + unicode_cap('rc')) def test_null_fileno(): @@ -90,66 +107,72 @@ def test_null_fileno(): This simulates piping output to another program. """ - out = stream=BytesIO() + out = StringIO() out.fileno = None - t = Terminal(stream=out) - eq_(t.save, ''.encode('utf-8')) + t = TestTerminal(stream=out) + eq_(t.save, u'') def test_mnemonic_colors(): """Make sure color shortcuts work.""" def color(num): - return tparm(tigetstr('setaf'), num) + return unicode_parm('setaf', num) def on_color(num): - return tparm(tigetstr('setab'), num) + return unicode_parm('setab', num) # Avoid testing red, blue, yellow, and cyan, since they might someday - # chance depending on terminal type. - t = Terminal() + # change depending on terminal type. + t = TestTerminal() eq_(t.white, color(7)) - bytes_eq(t.green, color(2)) # Make sure it's different than white. - bytes_eq(t.on_black, on_color(0)) - bytes_eq(t.on_green, on_color(2)) - bytes_eq(t.bright_black, color(8)) - bytes_eq(t.bright_green, color(10)) - bytes_eq(t.on_bright_black, on_color(8)) - bytes_eq(t.on_bright_green, on_color(10)) + 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_formatting_functions(): """Test crazy-ass formatting wrappers, both simple and compound.""" - t = Terminal(encoding='utf-8') - eq_(t.bold('hi'), t.bold + 'hi'.encode('utf-8') + t.normal) - eq_(t.green('hi'), t.green + 'hi'.encode('utf-8') + t.normal) - # Test encoding of unicodes: - eq_(t.bold_green(u'boö'), t.bold + t.green + u'boö'.encode('utf-8') + t.normal) + 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 + 'boo'.encode('utf-8') + t.normal) + 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 + 'meh'.encode('utf-8') + t.normal) + 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 = Terminal(stream=BytesIO()) - eq_(t.bold('hi'), 'hi'.encode('utf-8')) - eq_(t.green('hi'), 'hi'.encode('utf-8')) - # Test encoding of unicodes: - eq_(t.bold_green(u'boö'), u'boö'.encode('utf-8')) # unicode - eq_(t.bold_underline_green_on_red('boo'), 'boo'.encode('utf-8')) - eq_(t.on_bright_red_bold_bright_green_underline('meh'), 'meh'.encode('utf-8')) + 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 = Terminal() + 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: -- cgit v1.2.1 From fe38a088e7ea9d602ce327226ad93e977f2a30c2 Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Thu, 24 Nov 2011 20:13:24 -0800 Subject: Remove SPOT for version, because __init__ throws a SyntaxError when imported from setup.py. --- blessings/__init__.py | 4 +--- docs/conf.py | 2 +- setup.py | 6 ++---- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/blessings/__init__.py b/blessings/__init__.py index e83fb5c..67bbddb 100644 --- a/blessings/__init__.py +++ b/blessings/__init__.py @@ -6,9 +6,8 @@ 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 one in Python 2""" + """A dummy exception to take the place of Python 3's ``io.UnsupportedOperation`` in Python 2""" pass -import locale import os from os import isatty, environ import struct @@ -17,7 +16,6 @@ from termios import TIOCGWINSZ __all__ = ['Terminal'] -__version__ = (1, 1) class Terminal(object): diff --git a/docs/conf.py b/docs/conf.py index a1c2f49..040b29a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,7 +50,7 @@ copyright = u'2011, The Mozilla Foundation' # built documents. # # The short X.Y version. -version = '.'.join(str(i) for i in blessings.__version__) +version = '1.2' # The full version, including alpha/beta/rc tags. release = version diff --git a/setup.py b/setup.py index 4450589..617eb5f 100644 --- a/setup.py +++ b/setup.py @@ -2,8 +2,6 @@ import sys from setuptools import setup, find_packages -from blessings import __version__ - extra_setup = {} if sys.version_info >= (3,): @@ -11,7 +9,7 @@ if sys.version_info >= (3,): setup( name='blessings', - version='.'.join(str(i) for i in __version__), + version='1.2', description='A thin, practical wrapper around terminal formatting, positioning, and more', long_description=open('README.rst').read(), author='Erik Rose', @@ -37,6 +35,6 @@ setup( 'Topic :: Software Development :: User Interfaces', 'Topic :: Terminals' ], - keywords=['terminal', 'tty', 'curses', 'formatting', 'color', 'console'], + keywords=['terminal', 'tty', 'curses', 'ncurses', 'formatting', 'color', 'console'], **extra_setup ) -- cgit v1.2.1 From bf9dbec7fd5dfa05d5844e3c15291078f7c0b62f Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Sun, 27 Nov 2011 11:20:00 -0800 Subject: Make tox run Python 3 tests against the 2to3'd source. --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 66b1c22..e1753f2 100644 --- a/tox.ini +++ b/tox.ini @@ -2,5 +2,6 @@ envlist = py25, py26, py27, py32 [testenv] -commands = nosetests -deps=nose +commands = nosetests blessings +deps = nose +changedir = .tox # So Python 3 runs don't pick up incompatible, un-2to3'd source from the cwd -- cgit v1.2.1 From 381bfb79a7476a6e958b7d08d3a80de4c4cabeac Mon Sep 17 00:00:00 2001 From: Erik Rose Date: Sun, 27 Nov 2011 13:32:08 -0800 Subject: Explode more informatively if Python 3 version < 3.2.3. --- blessings/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/blessings/__init__.py b/blessings/__init__.py index 67bbddb..33b5cdc 100644 --- a/blessings/__init__.py +++ b/blessings/__init__.py @@ -10,11 +10,17 @@ except ImportError: 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'] -- cgit v1.2.1