Merge "sync latest strutils to python-cinderclient"

This commit is contained in:
Jenkins
2014-07-30 20:45:21 +00:00
committed by Gerrit Code Review
3 changed files with 267 additions and 138 deletions

View File

@@ -0,0 +1,17 @@
#
# 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.
import six
six.add_move(six.MovedModule('mox', 'mox', 'mox3.mox'))

View File

@@ -19,7 +19,7 @@ gettext for openstack-common modules.
Usual usage in an openstack.common module: Usual usage in an openstack.common module:
from cinderclient.openstack.common.gettextutils import _ from openstack.common.gettextutils import _
""" """
import copy import copy
@@ -27,18 +27,119 @@ import gettext
import locale import locale
from logging import handlers from logging import handlers
import os import os
import re
from babel import localedata from babel import localedata
import six import six
_localedir = os.environ.get('cinderclient'.upper() + '_LOCALEDIR')
_t = gettext.translation('cinderclient', localedir=_localedir, fallback=True)
_AVAILABLE_LANGUAGES = {} _AVAILABLE_LANGUAGES = {}
# FIXME(dhellmann): Remove this when moving to oslo.i18n.
USE_LAZY = False USE_LAZY = False
class TranslatorFactory(object):
"""Create translator functions
"""
def __init__(self, domain, localedir=None):
"""Establish a set of translation functions for the domain.
:param domain: Name of translation domain,
specifying a message catalog.
:type domain: str
:param lazy: Delays translation until a message is emitted.
Defaults to False.
:type lazy: Boolean
:param localedir: Directory with translation catalogs.
:type localedir: str
"""
self.domain = domain
if localedir is None:
localedir = os.environ.get(domain.upper() + '_LOCALEDIR')
self.localedir = localedir
def _make_translation_func(self, domain=None):
"""Return a new translation function ready for use.
Takes into account whether or not lazy translation is being
done.
The domain can be specified to override the default from the
factory, but the localedir from the factory is always used
because we assume the log-level translation catalogs are
installed in the same directory as the main application
catalog.
"""
if domain is None:
domain = self.domain
t = gettext.translation(domain,
localedir=self.localedir,
fallback=True)
# Use the appropriate method of the translation object based
# on the python version.
m = t.gettext if six.PY3 else t.ugettext
def f(msg):
"""oslo.i18n.gettextutils translation function."""
if USE_LAZY:
return Message(msg, domain=domain)
return m(msg)
return f
@property
def primary(self):
"The default translation function."
return self._make_translation_func()
def _make_log_translation_func(self, level):
return self._make_translation_func(self.domain + '-log-' + level)
@property
def log_info(self):
"Translate info-level log messages."
return self._make_log_translation_func('info')
@property
def log_warning(self):
"Translate warning-level log messages."
return self._make_log_translation_func('warning')
@property
def log_error(self):
"Translate error-level log messages."
return self._make_log_translation_func('error')
@property
def log_critical(self):
"Translate critical-level log messages."
return self._make_log_translation_func('critical')
# NOTE(dhellmann): When this module moves out of the incubator into
# oslo.i18n, these global variables can be moved to an integration
# module within each application.
# Create the global translation functions.
_translators = TranslatorFactory('cinderclient')
# The primary translation function using the well-known name "_"
_ = _translators.primary
# Translators for log levels.
#
# The abbreviated names are meant to reflect the usual use of a short
# name like '_'. The "L" is for "log" and the other letter comes from
# the level.
_LI = _translators.log_info
_LW = _translators.log_warning
_LE = _translators.log_error
_LC = _translators.log_critical
# NOTE(dhellmann): End of globals that will move to the application's
# integration module.
def enable_lazy(): def enable_lazy():
"""Convenience function for configuring _() to use lazy gettext """Convenience function for configuring _() to use lazy gettext
@@ -51,16 +152,7 @@ def enable_lazy():
USE_LAZY = True USE_LAZY = True
def _(msg): def install(domain):
if USE_LAZY:
return Message(msg, domain='cinderclient')
else:
if six.PY3:
return _t.gettext(msg)
return _t.ugettext(msg)
def install(domain, lazy=False):
"""Install a _() function using the given translation domain. """Install a _() function using the given translation domain.
Given a translation domain, install a _() function using gettext's Given a translation domain, install a _() function using gettext's
@@ -71,43 +163,14 @@ def install(domain, lazy=False):
a translation-domain-specific environment variable (e.g. a translation-domain-specific environment variable (e.g.
NOVA_LOCALEDIR). NOVA_LOCALEDIR).
Note that to enable lazy translation, enable_lazy must be
called.
:param domain: the translation domain :param domain: the translation domain
:param lazy: indicates whether or not to install the lazy _() function.
The lazy _() introduces a way to do deferred translation
of messages by installing a _ that builds Message objects,
instead of strings, which can then be lazily translated into
any available locale.
""" """
if lazy: from six import moves
# NOTE(mrodden): Lazy gettext functionality. tf = TranslatorFactory(domain)
# moves.builtins.__dict__['_'] = tf.primary
# The following introduces a deferred way to do translations on
# messages in OpenStack. We override the standard _() function
# and % (format string) operation to build Message objects that can
# later be translated when we have more information.
def _lazy_gettext(msg):
"""Create and return a Message object.
Lazy gettext function for a given domain, it is a factory method
for a project/module to get a lazy gettext function for its own
translation domain (i.e. nova, glance, cinder, etc.)
Message encapsulates a string so that we can translate
it later when needed.
"""
return Message(msg, domain=domain)
from six import moves
moves.builtins.__dict__['_'] = _lazy_gettext
else:
localedir = '%s_LOCALEDIR' % domain.upper()
if six.PY3:
gettext.install(domain,
localedir=os.environ.get(localedir))
else:
gettext.install(domain,
localedir=os.environ.get(localedir),
unicode=True)
class Message(six.text_type): class Message(six.text_type):
@@ -214,47 +277,22 @@ class Message(six.text_type):
if other is None: if other is None:
params = (other,) params = (other,)
elif isinstance(other, dict): elif isinstance(other, dict):
params = self._trim_dictionary_parameters(other) # Merge the dictionaries
# Copy each item in case one does not support deep copy.
params = {}
if isinstance(self.params, dict):
for key, val in self.params.items():
params[key] = self._copy_param(val)
for key, val in other.items():
params[key] = self._copy_param(val)
else: else:
params = self._copy_param(other) params = self._copy_param(other)
return params return params
def _trim_dictionary_parameters(self, dict_param):
"""Return a dict that only has matching entries in the msgid."""
# NOTE(luisg): Here we trim down the dictionary passed as parameters
# to avoid carrying a lot of unnecessary weight around in the message
# object, for example if someone passes in Message() % locals() but
# only some params are used, and additionally we prevent errors for
# non-deepcopyable objects by unicoding() them.
# Look for %(param) keys in msgid;
# Skip %% and deal with the case where % is first character on the line
keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid)
# If we don't find any %(param) keys but have a %s
if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid):
# Apparently the full dictionary is the parameter
params = self._copy_param(dict_param)
else:
params = {}
# Save our existing parameters as defaults to protect
# ourselves from losing values if we are called through an
# (erroneous) chain that builds a valid Message with
# arguments, and then does something like "msg % kwds"
# where kwds is an empty dictionary.
src = {}
if isinstance(self.params, dict):
src.update(self.params)
src.update(dict_param)
for key in keys:
params[key] = self._copy_param(src[key])
return params
def _copy_param(self, param): def _copy_param(self, param):
try: try:
return copy.deepcopy(param) return copy.deepcopy(param)
except TypeError: except Exception:
# Fallback to casting to unicode this will handle the # Fallback to casting to unicode this will handle the
# python code-like objects that can't be deep-copied # python code-like objects that can't be deep-copied
return six.text_type(param) return six.text_type(param)
@@ -266,13 +304,14 @@ class Message(six.text_type):
def __radd__(self, other): def __radd__(self, other):
return self.__add__(other) return self.__add__(other)
def __str__(self): if six.PY2:
# NOTE(luisg): Logging in python 2.6 tries to str() log records, def __str__(self):
# and it expects specifically a UnicodeError in order to proceed. # NOTE(luisg): Logging in python 2.6 tries to str() log records,
msg = _('Message objects do not support str() because they may ' # and it expects specifically a UnicodeError in order to proceed.
'contain non-ascii characters. ' msg = _('Message objects do not support str() because they may '
'Please use unicode() or translate() instead.') 'contain non-ascii characters. '
raise UnicodeError(msg) 'Please use unicode() or translate() instead.')
raise UnicodeError(msg)
def get_available_languages(domain): def get_available_languages(domain):
@@ -315,8 +354,8 @@ def get_available_languages(domain):
'zh_Hant_HK': 'zh_HK', 'zh_Hant_HK': 'zh_HK',
'zh_Hant': 'zh_TW', 'zh_Hant': 'zh_TW',
'fil': 'tl_PH'} 'fil': 'tl_PH'}
for (locale, alias) in six.iteritems(aliases): for (locale_, alias) in six.iteritems(aliases):
if locale in language_list and alias not in language_list: if locale_ in language_list and alias not in language_list:
language_list.append(alias) language_list.append(alias)
_AVAILABLE_LANGUAGES[domain] = language_list _AVAILABLE_LANGUAGES[domain] = language_list

View File

@@ -17,6 +17,7 @@
System-level utilities and helper functions. System-level utilities and helper functions.
""" """
import math
import re import re
import sys import sys
import unicodedata import unicodedata
@@ -26,16 +27,21 @@ import six
from cinderclient.openstack.common.gettextutils import _ from cinderclient.openstack.common.gettextutils import _
# Used for looking up extensions of text UNIT_PREFIX_EXPONENT = {
# to their 'multiplied' byte amount 'k': 1,
BYTE_MULTIPLIERS = { 'K': 1,
'': 1, 'Ki': 1,
't': 1024 ** 4, 'M': 2,
'g': 1024 ** 3, 'Mi': 2,
'm': 1024 ** 2, 'G': 3,
'k': 1024, 'Gi': 3,
'T': 4,
'Ti': 4,
}
UNIT_SYSTEM_INFO = {
'IEC': (1024, re.compile(r'(^[-+]?\d*\.?\d+)([KMGT]i?)?(b|bit|B)$')),
'SI': (1000, re.compile(r'(^[-+]?\d*\.?\d+)([kMGT])?(b|bit|B)$')),
} }
BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)')
TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes')
FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no')
@@ -44,6 +50,28 @@ SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]")
SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+") SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+")
# NOTE(flaper87): The following 3 globals are used by `mask_password`
_SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password']
# NOTE(ldbragst): Let's build a list of regex objects using the list of
# _SANITIZE_KEYS we already have. This way, we only have to add the new key
# to the list of _SANITIZE_KEYS and we can generate regular expressions
# for XML and JSON automatically.
_SANITIZE_PATTERNS = []
_FORMAT_PATTERNS = [r'(%(key)s\s*[=]\s*[\"\']).*?([\"\'])',
r'(<%(key)s>).*?(</%(key)s>)',
r'([\"\']%(key)s[\"\']\s*:\s*[\"\']).*?([\"\'])',
r'([\'"].*?%(key)s[\'"]\s*:\s*u?[\'"]).*?([\'"])',
r'([\'"].*?%(key)s[\'"]\s*,\s*\'--?[A-z]+\'\s*,\s*u?[\'"])'
'.*?([\'"])',
r'(%(key)s\s*--?[A-z]+\s*)\S+(\s*)']
for key in _SANITIZE_KEYS:
for pattern in _FORMAT_PATTERNS:
reg_ex = re.compile(pattern % {'key': key}, re.DOTALL)
_SANITIZE_PATTERNS.append(reg_ex)
def int_from_bool_as_string(subject): def int_from_bool_as_string(subject):
"""Interpret a string as a boolean and return either 1 or 0. """Interpret a string as a boolean and return either 1 or 0.
@@ -72,7 +100,7 @@ def bool_from_string(subject, strict=False, default=False):
Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
""" """
if not isinstance(subject, six.string_types): if not isinstance(subject, six.string_types):
subject = str(subject) subject = six.text_type(subject)
lowered = subject.strip().lower() lowered = subject.strip().lower()
@@ -92,7 +120,8 @@ def bool_from_string(subject, strict=False, default=False):
def safe_decode(text, incoming=None, errors='strict'): def safe_decode(text, incoming=None, errors='strict'):
"""Decodes incoming str using `incoming` if they're not already unicode. """Decodes incoming text/bytes string using `incoming` if they're not
already unicode.
:param incoming: Text's current encoding :param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid :param errors: Errors handling policy. See here for valid
@@ -101,7 +130,7 @@ def safe_decode(text, incoming=None, errors='strict'):
representation of it. representation of it.
:raises TypeError: If text is not an instance of str :raises TypeError: If text is not an instance of str
""" """
if not isinstance(text, six.string_types): if not isinstance(text, (six.string_types, six.binary_type)):
raise TypeError("%s can't be decoded" % type(text)) raise TypeError("%s can't be decoded" % type(text))
if isinstance(text, six.text_type): if isinstance(text, six.text_type):
@@ -131,7 +160,7 @@ def safe_decode(text, incoming=None, errors='strict'):
def safe_encode(text, incoming=None, def safe_encode(text, incoming=None,
encoding='utf-8', errors='strict'): encoding='utf-8', errors='strict'):
"""Encodes incoming str/unicode using `encoding`. """Encodes incoming text/bytes string using `encoding`.
If incoming is not specified, text is expected to be encoded with If incoming is not specified, text is expected to be encoded with
current python's default encoding. (`sys.getdefaultencoding`) current python's default encoding. (`sys.getdefaultencoding`)
@@ -144,7 +173,7 @@ def safe_encode(text, incoming=None,
representation of it. representation of it.
:raises TypeError: If text is not an instance of str :raises TypeError: If text is not an instance of str
""" """
if not isinstance(text, six.string_types): if not isinstance(text, (six.string_types, six.binary_type)):
raise TypeError("%s can't be encoded" % type(text)) raise TypeError("%s can't be encoded" % type(text))
if not incoming: if not incoming:
@@ -152,49 +181,59 @@ def safe_encode(text, incoming=None,
sys.getdefaultencoding()) sys.getdefaultencoding())
if isinstance(text, six.text_type): if isinstance(text, six.text_type):
if six.PY3: return text.encode(encoding, errors)
return text.encode(encoding, errors).decode(incoming)
else:
return text.encode(encoding, errors)
elif text and encoding != incoming: elif text and encoding != incoming:
# Decode text before encoding it with `encoding` # Decode text before encoding it with `encoding`
text = safe_decode(text, incoming, errors) text = safe_decode(text, incoming, errors)
if six.PY3: return text.encode(encoding, errors)
return text.encode(encoding, errors).decode(incoming) else:
else: return text
return text.encode(encoding, errors)
return text
def to_bytes(text, default=0): def string_to_bytes(text, unit_system='IEC', return_int=False):
"""Converts a string into an integer of bytes. """Converts a string into an float representation of bytes.
Looks at the last characters of the text to determine The units supported for IEC ::
what conversion is needed to turn the input text into a byte number.
Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive) Kb(it), Kib(it), Mb(it), Mib(it), Gb(it), Gib(it), Tb(it), Tib(it)
KB, KiB, MB, MiB, GB, GiB, TB, TiB
The units supported for SI ::
kb(it), Mb(it), Gb(it), Tb(it)
kB, MB, GB, TB
Note that the SI unit system does not support capital letter 'K'
:param text: String input for bytes size conversion. :param text: String input for bytes size conversion.
:param default: Default return value when text is blank. :param unit_system: Unit system for byte size conversion.
:param return_int: If True, returns integer representation of text
in bytes. (default: decimal)
:returns: Numerical representation of text in bytes.
:raises ValueError: If text has an invalid value.
""" """
match = BYTE_REGEX.search(text) try:
base, reg_ex = UNIT_SYSTEM_INFO[unit_system]
except KeyError:
msg = _('Invalid unit system: "%s"') % unit_system
raise ValueError(msg)
match = reg_ex.match(text)
if match: if match:
magnitude = int(match.group(1)) magnitude = float(match.group(1))
mult_key_org = match.group(2) unit_prefix = match.group(2)
if not mult_key_org: if match.group(3) in ['b', 'bit']:
return magnitude magnitude /= 8
elif text:
msg = _('Invalid string format: %s') % text
raise TypeError(msg)
else: else:
return default msg = _('Invalid string format: %s') % text
mult_key = mult_key_org.lower().replace('b', '', 1) raise ValueError(msg)
multiplier = BYTE_MULTIPLIERS.get(mult_key) if not unit_prefix:
if multiplier is None: res = magnitude
msg = _('Unknown byte multiplier: %s') % mult_key_org else:
raise TypeError(msg) res = magnitude * pow(base, UNIT_PREFIX_EXPONENT[unit_prefix])
return magnitude * multiplier if return_int:
return int(math.ceil(res))
return res
def to_slug(value, incoming=None, errors="strict"): def to_slug(value, incoming=None, errors="strict"):
@@ -220,3 +259,37 @@ def to_slug(value, incoming=None, errors="strict"):
"ascii", "ignore").decode("ascii") "ascii", "ignore").decode("ascii")
value = SLUGIFY_STRIP_RE.sub("", value).strip().lower() value = SLUGIFY_STRIP_RE.sub("", value).strip().lower()
return SLUGIFY_HYPHENATE_RE.sub("-", value) return SLUGIFY_HYPHENATE_RE.sub("-", value)
def mask_password(message, secret="***"):
"""Replace password with 'secret' in message.
:param message: The string which includes security information.
:param secret: value with which to replace passwords.
:returns: The unicode value of message with the password fields masked.
For example:
>>> mask_password("'adminPass' : 'aaaaa'")
"'adminPass' : '***'"
>>> mask_password("'admin_pass' : 'aaaaa'")
"'admin_pass' : '***'"
>>> mask_password('"password" : "aaaaa"')
'"password" : "***"'
>>> mask_password("'original_password' : 'aaaaa'")
"'original_password' : '***'"
>>> mask_password("u'original_password' : u'aaaaa'")
"u'original_password' : u'***'"
"""
message = six.text_type(message)
# NOTE(ldbragst): Check to see if anything in message contains any key
# specified in _SANITIZE_KEYS, if not then just return the message since
# we don't have to mask any passwords.
if not any(key in message for key in _SANITIZE_KEYS):
return message
secret = r'\g<1>' + secret + r'\g<2>'
for pattern in _SANITIZE_PATTERNS:
message = re.sub(pattern, secret, message)
return message