Fix default encoding issue with python 2.6
This change addresses issue #38: "fix unicode handling issues". The issue was originally reported against neutron client (https://bugs.launchpad.net/python-neutronclient/+bug/1189112) but was tracked down to the fact that python 2.6 does not set the default encoding for sys.stdout properly. A change to python 2.7 fixes the problem there and later (http://hg.python.org/cpython/rev/e60ef17561dc/), but since cliff supports python 2.6 it needs to handle the case explicitly. Change-Id: Id06507d78c7c82b25f39366ea4a6dfa4ef3a3a97
This commit is contained in:
		
							
								
								
									
										25
									
								
								cliff/app.py
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								cliff/app.py
									
									
									
									
									
								
							@@ -2,6 +2,8 @@
 | 
				
			|||||||
"""
 | 
					"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import argparse
 | 
					import argparse
 | 
				
			||||||
 | 
					import codecs
 | 
				
			||||||
 | 
					import locale
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
import logging.handlers
 | 
					import logging.handlers
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
@@ -67,14 +69,31 @@ class App(object):
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        self.command_manager = command_manager
 | 
					        self.command_manager = command_manager
 | 
				
			||||||
        self.command_manager.add_command('help', HelpCommand)
 | 
					        self.command_manager.add_command('help', HelpCommand)
 | 
				
			||||||
        self.stdin = stdin or sys.stdin
 | 
					        self._set_streams(stdin, stdout, stderr)
 | 
				
			||||||
        self.stdout = stdout or sys.stdout
 | 
					 | 
				
			||||||
        self.stderr = stderr or sys.stderr
 | 
					 | 
				
			||||||
        self.interactive_app_factory = interactive_app_factory
 | 
					        self.interactive_app_factory = interactive_app_factory
 | 
				
			||||||
        self.parser = self.build_option_parser(description, version)
 | 
					        self.parser = self.build_option_parser(description, version)
 | 
				
			||||||
        self.interactive_mode = False
 | 
					        self.interactive_mode = False
 | 
				
			||||||
        self.interpreter = None
 | 
					        self.interpreter = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def _set_streams(self, stdin, stdout, stderr):
 | 
				
			||||||
 | 
					        locale.setlocale(locale.LC_ALL, '')
 | 
				
			||||||
 | 
					        if sys.version_info[:2] == (2, 6):
 | 
				
			||||||
 | 
					            # Configure the input and output streams. If a stream is
 | 
				
			||||||
 | 
					            # provided, it must be configured correctly by the
 | 
				
			||||||
 | 
					            # caller. If not, make sure the versions of the standard
 | 
				
			||||||
 | 
					            # streams used by default are wrapped with encodings. This
 | 
				
			||||||
 | 
					            # works around a problem with Python 2.6 fixed in 2.7 and
 | 
				
			||||||
 | 
					            # later (http://hg.python.org/cpython/rev/e60ef17561dc/).
 | 
				
			||||||
 | 
					            lang, encoding = locale.getdefaultlocale()
 | 
				
			||||||
 | 
					            encoding = getattr(sys.stdout, 'encoding', None) or encoding
 | 
				
			||||||
 | 
					            self.stdin = stdin or codecs.getreader(encoding)(sys.stdin)
 | 
				
			||||||
 | 
					            self.stdout = stdout or codecs.getwriter(encoding)(sys.stdout)
 | 
				
			||||||
 | 
					            self.stderr = stderr or codecs.getwriter(encoding)(sys.stderr)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            self.stdin = stdin or sys.stdin
 | 
				
			||||||
 | 
					            self.stdout = stdout or sys.stdout
 | 
				
			||||||
 | 
					            self.stderr = stderr or sys.stderr
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def build_option_parser(self, description, version,
 | 
					    def build_option_parser(self, description, version,
 | 
				
			||||||
                            argparse_kwargs=None):
 | 
					                            argparse_kwargs=None):
 | 
				
			||||||
        """Return an argparse option parser for this application.
 | 
					        """Return an argparse option parser for this application.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,10 @@ class TableFormatter(ListFormatter, SingleFormatter):
 | 
				
			|||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def emit_list(self, column_names, data, stdout, parsed_args):
 | 
					    def emit_list(self, column_names, data, stdout, parsed_args):
 | 
				
			||||||
        x = prettytable.PrettyTable(column_names, print_empty=False)
 | 
					        x = prettytable.PrettyTable(
 | 
				
			||||||
 | 
					            column_names,
 | 
				
			||||||
 | 
					            print_empty=False,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        x.padding_width = 1
 | 
					        x.padding_width = 1
 | 
				
			||||||
        # Figure out the types of the columns in the
 | 
					        # Figure out the types of the columns in the
 | 
				
			||||||
        # first row and set the alignment of the
 | 
					        # first row and set the alignment of the
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										0
									
								
								cliff/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								cliff/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -1,11 +1,19 @@
 | 
				
			|||||||
 | 
					# -*- encoding: utf-8 -*-
 | 
				
			||||||
from argparse import ArgumentError
 | 
					from argparse import ArgumentError
 | 
				
			||||||
 | 
					try:
 | 
				
			||||||
 | 
					    from StringIO import StringIO
 | 
				
			||||||
 | 
					except ImportError:
 | 
				
			||||||
 | 
					    # Probably python 3, that test won't be run so ignore the error
 | 
				
			||||||
 | 
					    pass
 | 
				
			||||||
 | 
					import sys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import nose
 | 
				
			||||||
 | 
					import mock
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from cliff.app import App
 | 
					from cliff.app import App
 | 
				
			||||||
from cliff.command import Command
 | 
					from cliff.command import Command
 | 
				
			||||||
from cliff.commandmanager import CommandManager
 | 
					from cliff.commandmanager import CommandManager
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import mock
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
def make_app():
 | 
					def make_app():
 | 
				
			||||||
    cmd_mgr = CommandManager('cliff.tests')
 | 
					    cmd_mgr = CommandManager('cliff.tests')
 | 
				
			||||||
@@ -227,3 +235,115 @@ def test_option_parser_conflicting_option_custom_arguments_should_not_throw():
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    MyApp()
 | 
					    MyApp()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_output_encoding_default():
 | 
				
			||||||
 | 
					    # The encoding should come from getdefaultlocale() because
 | 
				
			||||||
 | 
					    # stdout has no encoding set.
 | 
				
			||||||
 | 
					    if sys.version_info[:2] != (2, 6):
 | 
				
			||||||
 | 
					        raise nose.SkipTest('only needed for python 2.6')
 | 
				
			||||||
 | 
					    data = '\xc3\xa9'
 | 
				
			||||||
 | 
					    u_data = data.decode('utf-8')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class MyApp(App):
 | 
				
			||||||
 | 
					        def __init__(self):
 | 
				
			||||||
 | 
					            super(MyApp, self).__init__(
 | 
				
			||||||
 | 
					                description='testing',
 | 
				
			||||||
 | 
					                version='0.1',
 | 
				
			||||||
 | 
					                command_manager=CommandManager('tests'),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    stdout = StringIO()
 | 
				
			||||||
 | 
					    getdefaultlocale = lambda: ('ignored', 'utf-8')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with mock.patch('sys.stdout', stdout):
 | 
				
			||||||
 | 
					        with mock.patch('locale.getdefaultlocale', getdefaultlocale):
 | 
				
			||||||
 | 
					            app = MyApp()
 | 
				
			||||||
 | 
					            app.stdout.write(u_data)
 | 
				
			||||||
 | 
					            actual = stdout.getvalue()
 | 
				
			||||||
 | 
					            assert data == actual
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_output_encoding_sys():
 | 
				
			||||||
 | 
					    # The encoding should come from sys.stdout because it is set
 | 
				
			||||||
 | 
					    # there.
 | 
				
			||||||
 | 
					    if sys.version_info[:2] != (2, 6):
 | 
				
			||||||
 | 
					        raise nose.SkipTest('only needed for python 2.6')
 | 
				
			||||||
 | 
					    data = '\xc3\xa9'
 | 
				
			||||||
 | 
					    u_data = data.decode('utf-8')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class MyApp(App):
 | 
				
			||||||
 | 
					        def __init__(self):
 | 
				
			||||||
 | 
					            super(MyApp, self).__init__(
 | 
				
			||||||
 | 
					                description='testing',
 | 
				
			||||||
 | 
					                version='0.1',
 | 
				
			||||||
 | 
					                command_manager=CommandManager('tests'),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    stdout = StringIO()
 | 
				
			||||||
 | 
					    stdout.encoding = 'utf-8'
 | 
				
			||||||
 | 
					    getdefaultlocale = lambda: ('ignored', 'utf-16')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with mock.patch('sys.stdout', stdout):
 | 
				
			||||||
 | 
					        with mock.patch('locale.getdefaultlocale', getdefaultlocale):
 | 
				
			||||||
 | 
					            app = MyApp()
 | 
				
			||||||
 | 
					            app.stdout.write(u_data)
 | 
				
			||||||
 | 
					            actual = stdout.getvalue()
 | 
				
			||||||
 | 
					            assert data == actual
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_error_encoding_default():
 | 
				
			||||||
 | 
					    # The encoding should come from getdefaultlocale() because
 | 
				
			||||||
 | 
					    # stdout has no encoding set.
 | 
				
			||||||
 | 
					    if sys.version_info[:2] != (2, 6):
 | 
				
			||||||
 | 
					        raise nose.SkipTest('only needed for python 2.6')
 | 
				
			||||||
 | 
					    data = '\xc3\xa9'
 | 
				
			||||||
 | 
					    u_data = data.decode('utf-8')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class MyApp(App):
 | 
				
			||||||
 | 
					        def __init__(self):
 | 
				
			||||||
 | 
					            super(MyApp, self).__init__(
 | 
				
			||||||
 | 
					                description='testing',
 | 
				
			||||||
 | 
					                version='0.1',
 | 
				
			||||||
 | 
					                command_manager=CommandManager('tests'),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    stderr = StringIO()
 | 
				
			||||||
 | 
					    getdefaultlocale = lambda: ('ignored', 'utf-8')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with mock.patch('sys.stderr', stderr):
 | 
				
			||||||
 | 
					        with mock.patch('locale.getdefaultlocale', getdefaultlocale):
 | 
				
			||||||
 | 
					            app = MyApp()
 | 
				
			||||||
 | 
					            app.stderr.write(u_data)
 | 
				
			||||||
 | 
					            actual = stderr.getvalue()
 | 
				
			||||||
 | 
					            assert data == actual
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_error_encoding_sys():
 | 
				
			||||||
 | 
					    # The encoding should come from sys.stdout (not sys.stderr)
 | 
				
			||||||
 | 
					    # because it is set there.
 | 
				
			||||||
 | 
					    if sys.version_info[:2] != (2, 6):
 | 
				
			||||||
 | 
					        raise nose.SkipTest('only needed for python 2.6')
 | 
				
			||||||
 | 
					    data = '\xc3\xa9'
 | 
				
			||||||
 | 
					    u_data = data.decode('utf-8')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    class MyApp(App):
 | 
				
			||||||
 | 
					        def __init__(self):
 | 
				
			||||||
 | 
					            super(MyApp, self).__init__(
 | 
				
			||||||
 | 
					                description='testing',
 | 
				
			||||||
 | 
					                version='0.1',
 | 
				
			||||||
 | 
					                command_manager=CommandManager('tests'),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    stdout = StringIO()
 | 
				
			||||||
 | 
					    stdout.encoding = 'utf-8'
 | 
				
			||||||
 | 
					    stderr = StringIO()
 | 
				
			||||||
 | 
					    getdefaultlocale = lambda: ('ignored', 'utf-16')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    with mock.patch('sys.stdout', stdout):
 | 
				
			||||||
 | 
					        with mock.patch('sys.stderr', stderr):
 | 
				
			||||||
 | 
					            with mock.patch('locale.getdefaultlocale', getdefaultlocale):
 | 
				
			||||||
 | 
					                app = MyApp()
 | 
				
			||||||
 | 
					                app.stderr.write(u_data)
 | 
				
			||||||
 | 
					                actual = stderr.getvalue()
 | 
				
			||||||
 | 
					                assert data == actual
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								demoapp/cliffdemo/encoding.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								demoapp/cliffdemo/encoding.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
				
			|||||||
 | 
					# -*- encoding: utf-8 -*-
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from cliff.lister import Lister
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Encoding(Lister):
 | 
				
			||||||
 | 
					    """Show some unicode text
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def take_action(self, parsed_args):
 | 
				
			||||||
 | 
					        messages = [
 | 
				
			||||||
 | 
					            u'pi: π',
 | 
				
			||||||
 | 
					            u'GB18030:鼀丅㐀ٸཌྷᠧꌢ€',
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            ('UTF-8', 'Unicode'),
 | 
				
			||||||
 | 
					            [(repr(t.encode('utf-8')), t)
 | 
				
			||||||
 | 
					             for t in messages],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
@@ -68,6 +68,7 @@ setup(
 | 
				
			|||||||
            'files = cliffdemo.list:Files',
 | 
					            'files = cliffdemo.list:Files',
 | 
				
			||||||
            'file = cliffdemo.show:File',
 | 
					            'file = cliffdemo.show:File',
 | 
				
			||||||
            'show file = cliffdemo.show:File',
 | 
					            'show file = cliffdemo.show:File',
 | 
				
			||||||
 | 
					            'unicode = cliffdemo.encoding:Encoding',
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user