# Copyright (c) 2011-2015, Matt Good <matt@matt-good.net>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
# list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# Copied from https://github.com/mgood/jprops
# For java properties file loading
import io
import re
import string
import sys
import time
PY2 = sys.version_info[0] == 2
if not PY2:
text_type = str
string_types = (str,)
unichr = chr
else:
text_type = unicode
string_types = (str, unicode)
unichr = unichr
class _CommentSentinel(object):
__slots__ = ()
def __repr__(self):
return 'jprops.COMMENT'
def __copy__(self):
return self
def __deepcopy__(self, memo):
return self
def __reduce__(self):
return 'COMMENT'
COMMENT = _CommentSentinel()
[docs]def load_properties(fh, mapping=dict):
"""
Reads properties from a Java .properties file.
Returns a dict (or provided mapping) of properties.
:param fh: a readable file-like object
:param mapping: mapping type to load properties into
"""
return mapping(iter_properties(fh))
[docs]def store_properties(fh, props, comment=None, timestamp=True):
"""
Writes properties to the file in Java properties format.
:param fh: a writable file-like object
:param props: a mapping (dict) or iterable of key/value pairs
:param comment: comment to write to the beginning of the file
:param timestamp: boolean indicating whether to write a timestamp comment
"""
w = _property_writer(fh)
if comment is not None:
w.write_comment(comment)
if timestamp:
w.write_comment(time.strftime('%a %b %d %H:%M:%S %Z %Y'))
if hasattr(props, 'keys'):
for key in props:
w.write_property(key, props[key])
else:
for key, value in props:
w.write_property(key, value)
[docs]def write_property(fh, key, value):
"""
Write a single property to the file in Java properties format.
:param fh: a writable file-like object
:param key: the key to write
:param value: the value to write
"""
_property_writer(fh).write_property(key, value)
[docs]def iter_properties(fh, comments=False):
"""
Incrementally read properties from a Java .properties file.
Yields tuples of key/value pairs.
If ``comments`` is `True`, comments will be included with ``jprops.COMMENT``
in place of the key.
:param fh: a readable file-like object
:param comments: should include comments (default: False)
"""
for line in _property_lines(fh):
key, value = _split_key_value(line)
if key is not COMMENT:
key = _unescape(key)
elif not comments:
continue
yield key, _unescape(value)
################################################################################
# Helpers for property parsing/writing
################################################################################
_COMMENT_CHARS = u'#!'
_LINE_PATTERN = re.compile(r'^\s*(?P<body>.*?)(?P<backslashes>\\*)$')
_KEY_TERMINATORS_EXPLICIT = u'=:'
_KEY_TERMINATORS = _KEY_TERMINATORS_EXPLICIT + string.whitespace
_COMMENT_UNICODE_ESCAPE = re.compile(u'[\u0100-\uffff]')
_PROPERTY_UNICODE_ESCAPE = re.compile(u'[\u0000-\u0019\u007f-\uffff]')
_escapes = {
't': '\t',
'n': '\n',
'f': '\f',
'r': '\r',
}
_escapes_rev = dict((v, '\\' + k) for k,v in _escapes.items())
for c in '\\' + _COMMENT_CHARS + _KEY_TERMINATORS_EXPLICIT:
_escapes_rev.setdefault(c, '\\' + c)
def _unescape(value):
def unirepl(m):
backslashes = m.group(1)
charcode = m.group(2)
# if preceded by even number of backslashes, the \u is escaped
if len(backslashes) % 2 == 0:
return m.group(0)
c = unichr(int(charcode, 16))
# if unicode decodes to '\', re-escape it to unescape in the second step
if c == '\\':
c = u'\\\\'
return backslashes + c
value = re.sub(r'(\\+)u([0-9a-fA-F]{4})', unirepl, value)
def bslashrepl(m):
code = m.group(1)
return _escapes.get(code, code)
value = re.sub(r'\\(.)', bslashrepl, value)
# if not native string (e.g. PY2) try converting it back
if not isinstance(value, str):
try:
value = value.encode('ascii')
except UnicodeEncodeError:
# cannot be represented in ASCII so leave it as unicode type
pass
return value
def _escape_comment(comment):
comment = comment.replace('\r\n', '\n').replace('\r', '\n')
comment = re.sub(r'\n(?![#!])', '\n#', comment)
return u'#' + comment
def _escape_key(key):
return _escape(key, _KEY_TERMINATORS)
def _escape_value(value):
tail = value.lstrip()
if len(tail) == len(value):
return _escape(value)
if tail:
head = value[:-len(tail)]
else:
head = value
# escape any leading whitespace, but leave other spaces intact
return _escape(head, string.whitespace) + _escape(tail)
def _escape(value, chars=''):
escape_chars = set(_escapes_rev)
escape_chars.update(chars)
escape_pattern = '[%s]' % re.escape(''.join(escape_chars))
def esc(m):
c = m.group(0)
return _escapes_rev.get(c) or '\\' + c
value = re.sub(escape_pattern, esc, value)
return value
def _unicode_replace(m):
c = m.group(0)
return r'\u%.4x' % ord(c)
def _split_key_value(line):
if line[0] in _COMMENT_CHARS:
return COMMENT, line[1:]
escaped = False
key_buf = io.StringIO()
for idx, c in enumerate(line):
if not escaped and c in _KEY_TERMINATORS:
key_terminated_fully = c in _KEY_TERMINATORS_EXPLICIT
break
key_buf.write(c)
escaped = c == u'\\'
else:
# no key terminator, key is full line & value is blank
return line, u''
value = line[idx+1:].lstrip()
if not key_terminated_fully and value[:1] in _KEY_TERMINATORS_EXPLICIT:
value = value[1:].lstrip()
return key_buf.getvalue(), value
def _is_text_file(fp):
return (
isinstance(fp, io.TextIOBase)
or getattr(fp, 'encoding', None) is not None
)
def _read_lines(fp):
lines = iter(fp)
if not _is_text_file(fp):
lines = (line.decode('latin-1') for line in lines)
# if file was not opened with universal newline support convert the newlines
if 'U' not in getattr(fp, 'mode', ''):
lines = _universal_newlines(lines)
return lines
def _universal_newlines(lines):
for line in lines:
line = line.replace('\r\n', '\n').replace('\r', '\n')
for piece in line.split('\n'):
yield piece
def _property_lines(fp):
buf = io.StringIO()
for line in _read_lines(fp):
m = _LINE_PATTERN.match(line)
body = m.group('body')
backslashes = m.group('backslashes')
if len(backslashes) % 2 == 0:
body += backslashes
continuation = False
else:
body += backslashes[:-1]
continuation = True
if not body:
continue
buf.write(body)
if not continuation:
yield buf.getvalue()
buf = io.StringIO()
def _property_writer(fh):
if _is_text_file(fh):
return _TextPropertyWriter(fh)
else:
return _BytesPropertyWriter(fh)
def _require_string(value, name):
if isinstance(value, text_type):
return value
if isinstance(value, string_types):
# allow Python 2 native strings
return value.decode('latin-1')
valid_types = ' or '.join(cls.__name__ for cls in string_types)
raise TypeError('%s must be %s, but got: %s %r'
% (name, valid_types, type(value), value))
class _TextPropertyWriter(object):
_escape_comment = staticmethod(_escape_comment)
_escape_key = staticmethod(_escape_key)
_escape_value = staticmethod(_escape_value)
def __init__(self, fp):
self.fp = fp
def write_property(self, key, value):
if key is COMMENT:
self.write_comment(value)
return
key = _require_string(key, 'keys')
value = _require_string(value, 'values')
key = self._escape_key(key)
value = self._escape_value(value)
self._write(key)
self._write(u'=')
self._write(value)
self._write(u'\n')
def write_comment(self, comment):
comment = _require_string(comment, 'comments')
comment = self._escape_comment(comment)
self._write(comment)
self._write(u'\n')
def _write(self, data):
self.fp.write(data)
class _BytesPropertyWriter(_TextPropertyWriter):
def _write(self, data):
self.fp.write(data.encode('latin-1'))
def _escape_comment(self, comment):
comment = _TextPropertyWriter._escape_comment(comment)
return _COMMENT_UNICODE_ESCAPE.sub(_unicode_replace, comment)
def _escape_key(self, key):
key = _TextPropertyWriter._escape_key(key)
return _PROPERTY_UNICODE_ESCAPE.sub(_unicode_replace, key)
def _escape_value(self, value):
value = _TextPropertyWriter._escape_value(value)
return _PROPERTY_UNICODE_ESCAPE.sub(_unicode_replace, value)