summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2017-03-22 23:24:19 +0000
committerGerrit Code Review <review@openstack.org>2017-03-22 23:24:19 +0000
commit5dc9b9a88a8631a5c759f2a956db4dc266d0a3fd (patch)
tree78dc52027b51969583d18a93c0ef49120572d492
parent1084840ee9e750f76de96629591e1b9c40a72bc6 (diff)
parent4d4e630cb691a23d9d84cf2725da1dbac2884fee (diff)
downloadcliff-stable/mitaka.tar.gz
Merge "Avoid ASCII encoding errors when output is redirected" into stable/mitakamitaka-eol2.0.1stable/mitaka
-rw-r--r--cliff/app.py41
-rw-r--r--cliff/tests/test_app.py83
2 files changed, 124 insertions, 0 deletions
diff --git a/cliff/app.py b/cliff/app.py
index 18f8b90..55b5852 100644
--- a/cliff/app.py
+++ b/cliff/app.py
@@ -1,11 +1,13 @@
"""Application base class.
"""
+import codecs
import inspect
import locale
import logging
import logging.handlers
import os
+import six
import sys
from cliff import argparse
@@ -70,6 +72,45 @@ class App(object):
locale.setlocale(locale.LC_ALL, '')
except locale.Error:
pass
+
+ # Unicode must be encoded/decoded for text I/O streams, the
+ # correct encoding for the stream must be selected and it must
+ # be capable of handling the set of characters in the stream
+ # or Python will raise a codec error. The correct codec is
+ # selected based on the locale. Python2 uses the locales
+ # encoding but only when the I/O stream is attached to a
+ # terminal (TTY) otherwise it uses the default ASCII
+ # encoding. The effect is internationalized text written to
+ # the terminal works as expected but if command line output is
+ # redirected (file or pipe) the ASCII codec is used and the
+ # program aborts with a codec error.
+ #
+ # The default I/O streams stdin, stdout and stderr can be
+ # wrapped in a codec based on the locale thus assuring the
+ # users desired encoding is always used no matter the I/O
+ # destination. Python3 does this by default.
+ #
+ # If the caller supplies an I/O stream we use it unmodified on
+ # the assumption the caller has taken all responsibility for
+ # the stream. But with Python2 if the caller allows us to
+ # default the I/O streams to sys.stdin, sys.stdout and
+ # sys.stderr we apply the locales encoding just as Python3
+ # would do. We also check to make sure the main Python program
+ # has not already already wrapped sys.stdin, sys.stdout and
+ # sys.stderr as this is a common recommendation.
+
+ if six.PY2:
+ encoding = locale.getpreferredencoding()
+ if encoding:
+ if not (stdin or isinstance(sys.stdin, codecs.StreamReader)):
+ stdin = codecs.getreader(encoding)(sys.stdin)
+
+ if not (stdout or isinstance(sys.stdout, codecs.StreamWriter)):
+ stdout = codecs.getwriter(encoding)(sys.stdout)
+
+ if not (stderr or isinstance(sys.stderr, codecs.StreamWriter)):
+ stderr = codecs.getwriter(encoding)(sys.stderr)
+
self.stdin = stdin or sys.stdin
self.stdout = stdout or sys.stdout
self.stderr = stderr or sys.stderr
diff --git a/cliff/tests/test_app.py b/cliff/tests/test_app.py
index e7487ce..4e6503b 100644
--- a/cliff/tests/test_app.py
+++ b/cliff/tests/test_app.py
@@ -5,7 +5,11 @@ try:
except ImportError:
from io import StringIO
+import codecs
+import locale
import mock
+import six
+import sys
from cliff.app import App
from cliff.command import Command
@@ -398,3 +402,82 @@ def test_verbose():
pass
else:
raise Exception('Exception was not thrown')
+
+
+def test_io_streams():
+ cmd_mgr = CommandManager('cliff.tests')
+ io = mock.Mock()
+
+ if six.PY2:
+ stdin_save = sys.stdin
+ stdout_save = sys.stdout
+ stderr_save = sys.stderr
+ encoding = locale.getpreferredencoding() or 'utf-8'
+
+ app = App('no io streams', 1, cmd_mgr)
+ assert isinstance(app.stdin, codecs.StreamReader)
+ assert isinstance(app.stdout, codecs.StreamWriter)
+ assert isinstance(app.stderr, codecs.StreamWriter)
+
+ app = App('with stdin io stream', 1, cmd_mgr, stdin=io)
+ assert app.stdin is io
+ assert isinstance(app.stdout, codecs.StreamWriter)
+ assert isinstance(app.stderr, codecs.StreamWriter)
+
+ app = App('with stdout io stream', 1, cmd_mgr, stdout=io)
+ assert isinstance(app.stdin, codecs.StreamReader)
+ assert app.stdout is io
+ assert isinstance(app.stderr, codecs.StreamWriter)
+
+ app = App('with stderr io stream', 1, cmd_mgr, stderr=io)
+ assert isinstance(app.stdin, codecs.StreamReader)
+ assert isinstance(app.stdout, codecs.StreamWriter)
+ assert app.stderr is io
+
+ try:
+ sys.stdin = codecs.getreader(encoding)(sys.stdin)
+ app = App('with wrapped sys.stdin io stream', 1, cmd_mgr)
+ assert app.stdin is sys.stdin
+ assert isinstance(app.stdout, codecs.StreamWriter)
+ assert isinstance(app.stderr, codecs.StreamWriter)
+ finally:
+ sys.stdin = stdin_save
+
+ try:
+ sys.stdout = codecs.getwriter(encoding)(sys.stdout)
+ app = App('with wrapped stdout io stream', 1, cmd_mgr)
+ assert isinstance(app.stdin, codecs.StreamReader)
+ assert app.stdout is sys.stdout
+ assert isinstance(app.stderr, codecs.StreamWriter)
+ finally:
+ sys.stdout = stdout_save
+
+ try:
+ sys.stderr = codecs.getwriter(encoding)(sys.stderr)
+ app = App('with wrapped stderr io stream', 1, cmd_mgr)
+ assert isinstance(app.stdin, codecs.StreamReader)
+ assert isinstance(app.stdout, codecs.StreamWriter)
+ assert app.stderr is sys.stderr
+ finally:
+ sys.stderr = stderr_save
+
+ else:
+ app = App('no io streams', 1, cmd_mgr)
+ assert app.stdin is sys.stdin
+ assert app.stdout is sys.stdout
+ assert app.stderr is sys.stderr
+
+ app = App('with stdin io stream', 1, cmd_mgr, stdin=io)
+ assert app.stdin is io
+ assert app.stdout is sys.stdout
+ assert app.stderr is sys.stderr
+
+ app = App('with stdout io stream', 1, cmd_mgr, stdout=io)
+ assert app.stdin is sys.stdin
+ assert app.stdout is io
+ assert app.stderr is sys.stderr
+
+ app = App('with stderr io stream', 1, cmd_mgr, stderr=io)
+ assert app.stdin is sys.stdin
+ assert app.stdout is sys.stdout
+ assert app.stderr is io