Decodes input and encodes output

Currently cinderclient doesn't handle properly incoming and outgoing
encode / decode process. As a solution for this, this patch implements a
decoding process for all data incoming from the user side and encodes
everything going out of the client, i.e: http requests, prints, etc.

This patch introduces a new module (strutils.py) taken from
oslo-incubator in order to use 2 of the functions present in it:

About safe_(decode|encode):

    Both functions try to encode / decode the incoming text using the
    stdin encoding, fallback to python's default encoding if that
    returns None or to UTF-8 as the last option.

    In both functions only basestring objects are accepted and they both
    raise TypeError if an object of another type is passed.

About the general cinderclient changes:

    In order to better support non-ASCII characters, it is a good
    practice to use unicode interanlly and encode everything that has to
    go out. This patch aims to do that and introduces this behaviour in
    the client.

Testing:

    A good test (besides using tox) is to use cinder client with and
    without setting any locale (export LANG=).

Fixes bug: 1130572

Change-Id: Idb7d06954c29e003f68a0c4aa0b80ecc7017cbc9
This commit is contained in:
Flaper Fesp
2013-02-27 14:47:44 +01:00
parent 7369310622
commit 03a4806d97
4 changed files with 145 additions and 12 deletions

View File

@@ -0,0 +1,133 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
System-level utilities and helper functions.
"""
import logging
import sys
LOG = logging.getLogger(__name__)
def int_from_bool_as_string(subject):
"""
Interpret a string as a boolean and return either 1 or 0.
Any string value in:
('True', 'true', 'On', 'on', '1')
is interpreted as a boolean True.
Useful for JSON-decoded stuff and config file parsing
"""
return bool_from_string(subject) and 1 or 0
def bool_from_string(subject):
"""
Interpret a string as a boolean.
Any string value in:
('True', 'true', 'On', 'on', 'Yes', 'yes', '1')
is interpreted as a boolean True.
Useful for JSON-decoded stuff and config file parsing
"""
if isinstance(subject, bool):
return subject
if isinstance(subject, basestring):
if subject.strip().lower() in ('true', 'on', 'yes', '1'):
return True
return False
def safe_decode(text, incoming=None, errors='strict'):
"""
Decodes incoming str using `incoming` if they're
not already unicode.
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: text or a unicode `incoming` encoded
representation of it.
:raises TypeError: If text is not an isntance of basestring
"""
if not isinstance(text, basestring):
raise TypeError("%s can't be decoded" % type(text))
if isinstance(text, unicode):
return text
if not incoming:
incoming = (sys.stdin.encoding or
sys.getdefaultencoding())
try:
return text.decode(incoming, errors)
except UnicodeDecodeError:
# Note(flaper87) If we get here, it means that
# sys.stdin.encoding / sys.getdefaultencoding
# didn't return a suitable encoding to decode
# text. This happens mostly when global LANG
# var is not set correctly and there's no
# default encoding. In this case, most likely
# python will use ASCII or ANSI encoders as
# default encodings but they won't be capable
# of decoding non-ASCII characters.
#
# Also, UTF-8 is being used since it's an ASCII
# extension.
return text.decode('utf-8', errors)
def safe_encode(text, incoming=None,
encoding='utf-8', errors='strict'):
"""
Encodes incoming str/unicode using `encoding`. If
incoming is not specified, text is expected to
be encoded with current python's default encoding.
(`sys.getdefaultencoding`)
:param incoming: Text's current encoding
:param encoding: Expected encoding for text (Default UTF-8)
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: text or a bytestring `encoding` encoded
representation of it.
:raises TypeError: If text is not an isntance of basestring
"""
if not isinstance(text, basestring):
raise TypeError("%s can't be encoded" % type(text))
if not incoming:
incoming = (sys.stdin.encoding or
sys.getdefaultencoding())
if isinstance(text, unicode):
return text.encode(encoding, errors)
elif text and encoding != incoming:
# Decode text before encoding it with `encoding`
text = safe_decode(text, incoming, errors)
return text.encode(encoding, errors)
return text

View File

@@ -30,6 +30,7 @@ import logging
from cinderclient import client from cinderclient import client
from cinderclient import exceptions as exc from cinderclient import exceptions as exc
import cinderclient.extension import cinderclient.extension
from cinderclient.openstack.common import strutils
from cinderclient import utils from cinderclient import utils
from cinderclient.v1 import shell as shell_v1 from cinderclient.v1 import shell as shell_v1
from cinderclient.v2 import shell as shell_v2 from cinderclient.v2 import shell as shell_v2
@@ -486,13 +487,16 @@ class OpenStackHelpFormatter(argparse.HelpFormatter):
def main(): def main():
try: try:
OpenStackCinderShell().main(sys.argv[1:]) OpenStackCinderShell().main(map(strutils.safe_decode, sys.argv[1:]))
except KeyboardInterrupt: except KeyboardInterrupt:
print >> sys.stderr, "... terminating cinder client" print >> sys.stderr, "... terminating cinder client"
sys.exit(130) sys.exit(130)
except Exception, e: except Exception, e:
logger.debug(e, exc_info=1) logger.debug(e, exc_info=1)
print >> sys.stderr, "ERROR: %s" % e.message message = e.message
if not isinstance(message, basestring):
message = str(message)
print >> sys.stderr, "ERROR: %s" % strutils.safe_encode(message)
sys.exit(1) sys.exit(1)

View File

@@ -1,4 +1,3 @@
import locale
import os import os
import re import re
import sys import sys
@@ -7,6 +6,7 @@ import uuid
import prettytable import prettytable
from cinderclient import exceptions from cinderclient import exceptions
from cinderclient.openstack.common import strutils
def arg(*args, **kwargs): def arg(*args, **kwargs):
@@ -143,14 +143,14 @@ def print_list(objs, fields, formatters={}):
row.append(data) row.append(data)
pt.add_row(row) pt.add_row(row)
print pt.get_string(sortby=fields[0]) print strutils.safe_encode(pt.get_string(sortby=fields[0]))
def print_dict(d, property="Property"): def print_dict(d, property="Property"):
pt = prettytable.PrettyTable([property, 'Value'], caching=False) pt = prettytable.PrettyTable([property, 'Value'], caching=False)
pt.aligns = ['l', 'l'] pt.aligns = ['l', 'l']
[pt.add_row(list(r)) for r in d.iteritems()] [pt.add_row(list(r)) for r in d.iteritems()]
print pt.get_string(sortby=property) print strutils.safe_encode(pt.get_string(sortby=property))
def find_resource(manager, name_or_id): def find_resource(manager, name_or_id):
@@ -164,7 +164,7 @@ def find_resource(manager, name_or_id):
# now try to get entity as uuid # now try to get entity as uuid
try: try:
uuid.UUID(str(name_or_id)) uuid.UUID(strutils.safe_decode(name_or_id))
return manager.get(name_or_id) return manager.get(name_or_id)
except (ValueError, exceptions.NotFound): except (ValueError, exceptions.NotFound):
pass pass
@@ -180,11 +180,7 @@ def find_resource(manager, name_or_id):
return manager.find(name=name_or_id) return manager.find(name=name_or_id)
except exceptions.NotFound: except exceptions.NotFound:
try: try:
# For command-line arguments that are in Unicode return manager.find(display_name=name_or_id)
encoding = (locale.getpreferredencoding() or
sys.stdin.encoding or
'UTF-8')
return manager.find(display_name=(name_or_id.decode(encoding)))
except (UnicodeDecodeError, exceptions.NotFound): except (UnicodeDecodeError, exceptions.NotFound):
try: try:
# Volumes does not have name, but display_name # Volumes does not have name, but display_name

View File

@@ -1,7 +1,7 @@
[DEFAULT] [DEFAULT]
# The list of modules to copy from openstack-common # The list of modules to copy from openstack-common
modules=setup,version modules=setup,version,strutils
# The base module to hold the copy of openstack.common # The base module to hold the copy of openstack.common
base=cinderclient base=cinderclient