blob: c9e79888ade892c692d82171b3412c079d4a168a [file] [log] [blame]
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You 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.
"""
String formatting and string-based format utilities.
"""
import json
from types import MethodType
from .yaml import yaml # @UnresolvedImport
from .collections import FrozenList, FrozenDict, StrictList, StrictDict, OrderedDict
PLURALIZE_EXCEPTIONS = {}
# Add our types to ruamel.yaml (for round trips)
yaml.representer.RoundTripRepresenter.add_representer(
FrozenList, yaml.representer.RoundTripRepresenter.represent_list)
yaml.representer.RoundTripRepresenter.add_representer(
FrozenDict, yaml.representer.RoundTripRepresenter.represent_dict)
yaml.representer.RoundTripRepresenter.add_representer(
StrictList, yaml.representer.RoundTripRepresenter.represent_list)
yaml.representer.RoundTripRepresenter.add_representer(
StrictDict, yaml.representer.RoundTripRepresenter.represent_dict)
# Without this, ruamel.yaml will output "!!omap" types, which is
# technically correct but unnecessarily verbose for our uses
yaml.representer.RoundTripRepresenter.add_representer(
OrderedDict, yaml.representer.RoundTripRepresenter.represent_dict)
class JsonAsRawEncoder(json.JSONEncoder):
"""
A :class:`JSONEncoder` that will use the ``as_raw`` property of objects if available.
"""
def raw_encoder_default(self, obj):
try:
return iter(obj)
except TypeError:
if hasattr(obj, 'as_raw'):
return as_raw(obj)
return str(obj)
return super(JsonAsRawEncoder, self).default(obj)
def __init__(self, *args, **kwargs):
kwargs['default'] = self.raw_encoder_default
super(JsonAsRawEncoder, self).__init__(*args, **kwargs)
class YamlAsRawDumper(yaml.dumper.RoundTripDumper): # pylint: disable=too-many-ancestors
"""
A :class:`RoundTripDumper` that will use the ``as_raw`` property of objects if available.
"""
def represent_data(self, data):
if hasattr(data, 'as_raw'):
data = as_raw(data)
return super(YamlAsRawDumper, self).represent_data(data)
def decode_list(data):
decoded_list = []
for item in data:
if isinstance(item, unicode):
item = item.encode('utf-8')
elif isinstance(item, list):
item = decode_list(item)
elif isinstance(item, dict):
item = decode_dict(item)
decoded_list.append(item)
return decoded_list
def decode_dict(data):
decoded_dict = {}
for key, value in data.iteritems():
if isinstance(key, unicode):
key = key.encode('utf-8')
if isinstance(value, unicode):
value = value.encode('utf-8')
elif isinstance(value, list):
value = decode_list(value)
elif isinstance(value, dict):
value = decode_dict(value)
decoded_dict[key] = value
return decoded_dict
def safe_str(value):
"""
Like :class:`str` coercion, but makes sure that Unicode strings are properly encoded, and will
never return ``None``.
"""
try:
return str(value)
except UnicodeEncodeError:
return unicode(value).encode('utf8')
def safe_repr(value):
"""
Like :func:`repr`, but calls :func:`as_raw` and :func:`as_agnostic` first.
"""
return repr(as_agnostic(as_raw(value)))
def string_list_as_string(strings):
"""
Nice representation of a list of strings.
"""
if not strings:
return 'none'
return ', '.join('"{0}"'.format(safe_str(v)) for v in strings)
def pluralize(noun):
plural = PLURALIZE_EXCEPTIONS.get(noun)
if plural is not None:
return plural
elif noun.endswith('s'):
return '{0}es'.format(noun)
elif noun.endswith('y'):
return '{0}ies'.format(noun[:-1])
else:
return '{0}s'.format(noun)
def as_raw(value):
"""
Converts values using their ``as_raw`` property, if it exists, recursively.
"""
if hasattr(value, 'as_raw'):
value = value.as_raw
if isinstance(value, MethodType):
# Old-style Python classes don't support properties
value = value()
elif isinstance(value, list):
value = list(value)
for i, v in enumerate(value):
value[i] = as_raw(v)
elif isinstance(value, dict):
value = dict(value)
for k, v in value.iteritems():
value[k] = as_raw(v)
return value
def as_raw_list(value):
"""
Assuming value is a list, converts its values using :func:`as_raw`.
"""
if value is None:
return []
if isinstance(value, dict):
value = value.itervalues()
return [as_raw(v) for v in value]
def as_raw_dict(value):
"""
Assuming value is a dict, converts its values using :func:`as_raw`. The keys are left as is.
"""
if value is None:
return OrderedDict()
return OrderedDict((
(k, as_raw(v)) for k, v in value.iteritems()))
def as_agnostic(value):
"""
Converts subclasses of list and dict to standard lists and dicts, and Unicode strings to
non-Unicode if possible, recursively.
Useful for creating human-readable output of structures.
"""
if isinstance(value, unicode):
try:
value = str(value)
except UnicodeEncodeError:
pass
elif isinstance(value, list):
value = list(value)
elif isinstance(value, dict):
value = dict(value)
if isinstance(value, list):
for i, _ in enumerate(value):
value[i] = as_agnostic(value[i])
elif isinstance(value, dict):
for k, v in value.iteritems():
value[k] = as_agnostic(v)
return value
def json_dumps(value, indent=2):
"""
JSON dumps that supports Unicode and the ``as_raw`` property of objects if available.
"""
return json.dumps(value, indent=indent, ensure_ascii=False, cls=JsonAsRawEncoder)
def yaml_dumps(value, indent=2):
"""
YAML dumps that supports Unicode and the ``as_raw`` property of objects if available.
"""
return yaml.dump(value, indent=indent, allow_unicode=True, Dumper=YamlAsRawDumper)
def yaml_loads(value):
return yaml.load(value, Loader=yaml.SafeLoader)