Skip to content

Commit

Permalink
added support for serializing timedelta and relativedelta
Browse files Browse the repository at this point in the history
  • Loading branch information
gabi committed May 23, 2021
1 parent 07758b8 commit a826b18
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 25 deletions.
77 changes: 67 additions & 10 deletions pyhocon/converter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import re
import sys
from datetime import timedelta

from pyhocon import ConfigFactory
from pyhocon.config_tree import ConfigQuotedString
Expand All @@ -9,13 +10,17 @@
from pyhocon.config_tree import ConfigValues
from pyhocon.config_tree import NoneValue


try:
basestring
except NameError:
basestring = str
unicode = str

try:
from dateutil.relativedelta import relativedelta
except Exception:
relativedelta = None


class HOCONConverter(object):
@classmethod
Expand Down Expand Up @@ -55,6 +60,8 @@ def to_json(cls, config, compact=False, indent=2, level=0):
)
lines += ',\n'.join(bet_lines)
lines += '\n{indent}]'.format(indent=''.rjust(level * indent, ' '))
elif cls._is_timedelta_like(config):
lines += cls._timedelta_to_str(config)
elif isinstance(config, basestring):
lines = json.dumps(config, ensure_ascii=False)
elif config is None or isinstance(config, NoneValue):
Expand Down Expand Up @@ -130,6 +137,8 @@ def to_hocon(cls, config, compact=False, indent=2, level=0):
lines = '"""{value}"""'.format(value=config.value) # multilines
else:
lines = '"{value}"'.format(value=cls.__escape_string(config.value))
elif cls._is_timedelta_like(config):
lines += cls._timedelta_to_hocon(config)
elif config is None or isinstance(config, NoneValue):
lines = 'null'
elif config is True:
Expand Down Expand Up @@ -171,6 +180,8 @@ def to_yaml(cls, config, compact=False, indent=2, level=0):
bet_lines.append('{indent}- {value}'.format(indent=''.rjust(level * indent, ' '),
value=cls.to_yaml(item, compact, indent, level + 1)))
lines += '\n'.join(bet_lines)
elif cls._is_timedelta_like(config):
lines += cls._timedelta_to_str(config)
elif isinstance(config, basestring):
# if it contains a \n then it's multiline
lines = config.split('\n')
Expand All @@ -189,13 +200,14 @@ def to_yaml(cls, config, compact=False, indent=2, level=0):
return lines

@classmethod
def to_properties(cls, config, compact=False, indent=2, key_stack=[]):
def to_properties(cls, config, compact=False, indent=2, key_stack=None):
"""Convert HOCON input into a .properties output
:return: .properties string representation
:type return: basestring
:return:
"""
key_stack = key_stack or []

def escape_value(value):
return value.replace('=', '\\=').replace('!', '\\!').replace('#', '\\#').replace('\n', '\\\n')
Expand All @@ -210,6 +222,8 @@ def escape_value(value):
for index, item in enumerate(config):
if item is not None:
lines.append(cls.to_properties(item, compact, indent, stripped_key_stack + [str(index)]))
elif cls._is_timedelta_like(config):
lines.append('.'.join(stripped_key_stack) + ' = ' + cls._timedelta_to_str(config))
elif isinstance(config, basestring):
lines.append('.'.join(stripped_key_stack) + ' = ' + escape_value(config))
elif config is True:
Expand Down Expand Up @@ -264,15 +278,58 @@ def convert_from_file(cls, input_file=None, output_file=None, output_format='jso
def __escape_match(cls, match):
char = match.group(0)
return {
'\b': r'\b',
'\t': r'\t',
'\n': r'\n',
'\f': r'\f',
'\r': r'\r',
'"': r'\"',
'\\': r'\\',
}.get(char) or (r'\u%04x' % ord(char))
'\b': r'\b',
'\t': r'\t',
'\n': r'\n',
'\f': r'\f',
'\r': r'\r',
'"': r'\"',
'\\': r'\\',
}.get(char) or (r'\u%04x' % ord(char))

@classmethod
def __escape_string(cls, string):
return re.sub(r'[\x00-\x1F"\\]', cls.__escape_match, string)

@classmethod
def _is_timedelta_like(cls, config):
return isinstance(config, timedelta) or relativedelta is not None and isinstance(config, relativedelta)

@classmethod
def _timedelta_to_str(cls, config):
if isinstance(config, relativedelta):
time_delta = cls._relative_delta_to_timedelta(config)
else:
time_delta = config
return str(int(time_delta.total_seconds() * 1000))

@classmethod
def _timedelta_to_hocon(cls, config):
"""
:type config: timedelta
"""
if relativedelta is not None and isinstance(config, relativedelta):
if config.hours > 0:
return str(config.hours) + ' hours'
elif config.minutes > 0:
return str(config.minutes) + ' minutes'

if config.days > 0:
return str(config.days) + ' days'
elif config.seconds > 0:
return str(config.seconds) + ' seconds'
elif config.microseconds > 0:
return str(config.microseconds) + ' microseconds'
else:
return '0 seconds'

@classmethod
def _relative_delta_to_timedelta(cls, relative_delta):
"""
:type relative_delta: relativedelta
"""
return timedelta(days=relative_delta.days,
hours=relative_delta.hours,
minutes=relative_delta.minutes,
seconds=relative_delta.seconds,
microseconds=relative_delta.microseconds)
35 changes: 35 additions & 0 deletions tests/test_converter.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
# -*- encoding: utf-8 -*-
from datetime import timedelta

import pytest

from pyhocon import ConfigTree
from pyhocon.converter import HOCONConverter

try:
from dateutil.relativedelta import relativedelta
except Exception:
def relativedelta(**kwargs):
return None


def to_json(obj):
return HOCONConverter.to_json(ConfigTree(obj), compact=True, indent=1)
Expand Down Expand Up @@ -106,3 +115,29 @@ def test_format_multiline_string(self):
assert 'a = """\nc"""' == to_hocon({'a': '\nc'})
assert 'a = """b\n"""' == to_hocon({'a': 'b\n'})
assert 'a = """\n\n"""' == to_hocon({'a': '\n\n'})

@pytest.mark.parametrize('time_delta, expected_result',
(
(timedelta(days=0), 'td = 0 seconds'),
(timedelta(days=5), 'td = 5 days'),
(timedelta(seconds=51), 'td = 51 seconds'),
(timedelta(microseconds=786), 'td = 786 microseconds'),
)
)
def test_format_time_delta(self, time_delta, expected_result):
assert expected_result == to_hocon({'td': time_delta})

@pytest.mark.parametrize('time_delta, expected_result',
(
(relativedelta(seconds=0), 'td = 0 seconds'),
(relativedelta(hours=0), 'td = 0 seconds'),
(relativedelta(days=5), 'td = 5 days'),
(relativedelta(weeks=3), 'td = 21 days'),
(relativedelta(hours=2), 'td = 2 hours'),
(relativedelta(minutes=43), 'td = 43 minutes'),
)
)
@pytest.mark.skipif(relativedelta(hours=1) is None, reason='dateutils.relativedelta not available')
def test_format_relativedelta(self, time_delta, expected_result):
assert expected_result == to_hocon({'td': time_delta})
38 changes: 23 additions & 15 deletions tests/test_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class TestHOCONConverter(object):
h = null
i = {}
"a.b" = 2
td_days = 4 days
"""

CONFIG = ConfigFactory.parse_string(CONFIG_STRING)
Expand All @@ -41,7 +42,8 @@ class TestHOCONConverter(object):
"g": [],
"h": null,
"i": {},
"a.b": 2
"a.b": 2,
"td_days": 345600000
}
"""

Expand All @@ -63,6 +65,7 @@ class TestHOCONConverter(object):
h = null
i {}
"a.b" = 2
td_days = 4 days
"""

EXPECTED_COMPACT_HOCON = \
Expand All @@ -81,6 +84,7 @@ class TestHOCONConverter(object):
h = null
i {}
"a.b" = 2
td_days = 4 days
"""

EXPECTED_YAML = \
Expand All @@ -102,6 +106,7 @@ class TestHOCONConverter(object):
h: null
i:
a.b: 2
td_days: 345600000
"""

EXPECTED_PROPERTIES = \
Expand All @@ -117,32 +122,33 @@ class TestHOCONConverter(object):
f1 = true
f2 = false
a.b = 2
td_days = 345600000
"""

def test_to_json(self):
converted = HOCONConverter.to_json(TestHOCONConverter.CONFIG)
assert [line.strip() for line in TestHOCONConverter.EXPECTED_JSON.split('\n') if line.strip()]\
== [line.strip() for line in converted.split('\n') if line.strip()]
assert [line.strip() for line in TestHOCONConverter.EXPECTED_JSON.split('\n') if line.strip()] \
== [line.strip() for line in converted.split('\n') if line.strip()]

def test_to_yaml(self):
converted = HOCONConverter.to_yaml(TestHOCONConverter.CONFIG)
assert [line.strip() for line in TestHOCONConverter.EXPECTED_YAML.split('\n') if line.strip()]\
== [line.strip() for line in converted.split('\n') if line.strip()]
assert [line.strip() for line in TestHOCONConverter.EXPECTED_YAML.split('\n') if line.strip()] \
== [line.strip() for line in converted.split('\n') if line.strip()]

def test_to_properties(self):
converted = HOCONConverter.to_properties(TestHOCONConverter.CONFIG)
assert [line.strip() for line in TestHOCONConverter.EXPECTED_PROPERTIES.split('\n') if line.strip()]\
== [line.strip() for line in converted.split('\n') if line.strip()]
assert [line.strip() for line in TestHOCONConverter.EXPECTED_PROPERTIES.split('\n') if line.strip()] \
== [line.strip() for line in converted.split('\n') if line.strip()]

def test_to_hocon(self):
converted = HOCONConverter.to_hocon(TestHOCONConverter.CONFIG)
assert [line.strip() for line in TestHOCONConverter.EXPECTED_HOCON.split('\n') if line.strip()]\
== [line.strip() for line in converted.split('\n') if line.strip()]
assert [line.strip() for line in TestHOCONConverter.EXPECTED_HOCON.split('\n') if line.strip()] \
== [line.strip() for line in converted.split('\n') if line.strip()]

def test_to_compact_hocon(self):
converted = HOCONConverter.to_hocon(TestHOCONConverter.CONFIG, compact=True)
assert [line.strip() for line in TestHOCONConverter.EXPECTED_COMPACT_HOCON.split('\n') if line.strip()]\
== [line.strip() for line in converted.split('\n') if line.strip()]
assert [line.strip() for line in TestHOCONConverter.EXPECTED_COMPACT_HOCON.split('\n') if line.strip()] \
== [line.strip() for line in converted.split('\n') if line.strip()]

def _test_convert_from_file(self, input, expected_output, format):
with tempfile.NamedTemporaryFile('w') as fdin:
Expand All @@ -152,18 +158,20 @@ def _test_convert_from_file(self, input, expected_output, format):
HOCONConverter.convert_from_file(fdin.name, fdout.name, format)
with open(fdout.name) as fdi:
converted = fdi.read()
assert [line.strip() for line in expected_output.split('\n') if line.strip()]\
== [line.strip() for line in converted.split('\n') if line.strip()]
assert [line.strip() for line in expected_output.split('\n') if line.strip()] \
== [line.strip() for line in converted.split('\n') if line.strip()]

def test_convert_from_file(self):
self._test_convert_from_file(TestHOCONConverter.CONFIG_STRING, TestHOCONConverter.EXPECTED_JSON, 'json')
self._test_convert_from_file(TestHOCONConverter.CONFIG_STRING, TestHOCONConverter.EXPECTED_YAML, 'yaml')
self._test_convert_from_file(TestHOCONConverter.CONFIG_STRING, TestHOCONConverter.EXPECTED_PROPERTIES, 'properties')
self._test_convert_from_file(TestHOCONConverter.CONFIG_STRING, TestHOCONConverter.EXPECTED_PROPERTIES,
'properties')
self._test_convert_from_file(TestHOCONConverter.CONFIG_STRING, TestHOCONConverter.EXPECTED_HOCON, 'hocon')

def test_invalid_format(self):
with pytest.raises(Exception):
self._test_convert_from_file(TestHOCONConverter.CONFIG_STRING, TestHOCONConverter.EXPECTED_PROPERTIES, 'invalid')
self._test_convert_from_file(TestHOCONConverter.CONFIG_STRING, TestHOCONConverter.EXPECTED_PROPERTIES,
'invalid')


def test_substitutions_conversions():
Expand Down

0 comments on commit a826b18

Please sign in to comment.