diff options
author | Anthon van der Neut <anthon@mnt.org> | 2016-08-20 10:57:19 +0200 |
---|---|---|
committer | Anthon van der Neut <anthon@mnt.org> | 2016-08-20 10:57:19 +0200 |
commit | e826a227fac58b8c4fe312f948c42a2d458af9d2 (patch) | |
tree | 6d2862247f951882c1c49ad1a8791bd971b5b5b2 | |
parent | 95a952dc87635fd5c72153add15607a80ac5d02d (diff) | |
download | ruamel.yaml-e826a227fac58b8c4fe312f948c42a2d458af9d2.tar.gz |
fix issue 45: preserve datetime 'T' + timezone
-rw-r--r-- | README.rst | 9 | ||||
-rw-r--r-- | _test/roundtrip.py | 4 | ||||
-rw-r--r-- | _test/test_datetime.py | 121 | ||||
-rw-r--r-- | constructor.py | 68 | ||||
-rw-r--r-- | representer.py | 16 | ||||
-rw-r--r-- | timestamp.py | 13 |
6 files changed, 222 insertions, 9 deletions
@@ -18,6 +18,15 @@ ChangeLog :: + 0.12.5 (2016-08-20): + - fixing issue 45 preserving datetime formatting (submitted by altuin) + Several formatting parameters are preserved with some normalisation: + - preserve 'T', 't' is replaced by 'T', multiple spaces between date + and time reduced to one. + - optional space before timezone is removed + - still using microseconds, but now rounded (.1234567 -> .123457) + - Z/-5/+01:00 preserved + 0.12.4 (2016-08-19): - Fix for issue 44: missing preserve_quotes keyword argument (reported by M. Crusoe) diff --git a/_test/roundtrip.py b/_test/roundtrip.py index 9872239..d57e846 100644 --- a/_test/roundtrip.py +++ b/_test/roundtrip.py @@ -38,6 +38,10 @@ def round_trip_dump(data, indent=None, block_seq_indent=None, top_level_colon_al def round_trip(inp, outp=None, extra=None, intermediate=None, indent=None, block_seq_indent=None, top_level_colon_align=None, prefix_colon=None, preserve_quotes=None): + """ + inp: input string to parse + outp: expected output (equals input if not specified) + """ if outp is None: outp = inp doutp = dedent(outp) diff --git a/_test/test_datetime.py b/_test/test_datetime.py new file mode 100644 index 0000000..95f89d1 --- /dev/null +++ b/_test/test_datetime.py @@ -0,0 +1,121 @@ +# coding: utf-8 + +""" +http://yaml.org/type/timestamp.html specifies the regexp to use +for datetime.date and datetime.datetime construction. Date is simple +but datetime can have 'T' or 't' as well as 'Z' or a timezone offset (in +hours and minutes). This information was originally used to create +a UTC datetime and then discarded + +examples from the above: + +canonical: 2001-12-15T02:59:43.1Z +valid iso8601: 2001-12-14t21:59:43.10-05:00 +space separated: 2001-12-14 21:59:43.10 -5 +no time zone (Z): 2001-12-15 2:59:43.10 +date (00:00:00Z): 2002-12-14 + +Please note that a fraction can only be included if not equal to 0 + +""" + +import pytest # NOQA +import ruamel.yaml # NOQA + +from roundtrip import round_trip, dedent, round_trip_load, round_trip_dump # NOQA + + +class TestDateTime: + def test_date_only(self): + round_trip(""" + - 2011-10-02 + """, """ + - 2011-10-02 + """) + + def test_zero_fraction(self): + round_trip(""" + - 2011-10-02 16:45:00.0 + """, """ + - 2011-10-02 16:45:00 + """) + + def test_long_fraction(self): + round_trip(""" + - 2011-10-02 16:45:00.1234 # expand with zeros + - 2011-10-02 16:45:00.123456 + - 2011-10-02 16:45:00.12345612 # round to microseconds + - 2011-10-02 16:45:00.1234565 # round up + - 2011-10-02 16:45:00.12345678 # round up + """, """ + - 2011-10-02 16:45:00.123400 # expand with zeros + - 2011-10-02 16:45:00.123456 + - 2011-10-02 16:45:00.123456 # round to microseconds + - 2011-10-02 16:45:00.123457 # round up + - 2011-10-02 16:45:00.123457 # round up + """) + + def test_canonical(self): + round_trip(""" + - 2011-10-02T16:45:00.1Z + """, """ + - 2011-10-02T16:45:00.100000Z + """) + + def test_spaced_timezone(self): + round_trip(""" + - 2011-10-02T11:45:00 -5 + """, """ + - 2011-10-02T11:45:00-5 + """) + + def test_normal_timezone(self): + round_trip(""" + - 2011-10-02T11:45:00-5 + - 2011-10-02 11:45:00-5 + - 2011-10-02T11:45:00-05:00 + - 2011-10-02 11:45:00-05:00 + """) + + def test_no_timezone(self): + round_trip(""" + - 2011-10-02 6:45:00 + """, """ + - 2011-10-02 06:45:00 + """) + + def test_explicit_T(self): + round_trip(""" + - 2011-10-02T16:45:00 + """, """ + - 2011-10-02T16:45:00 + """) + + def test_explicit_t(self): # to upper + round_trip(""" + - 2011-10-02t16:45:00 + """, """ + - 2011-10-02T16:45:00 + """) + + def test_no_T_multi_space(self): + round_trip(""" + - 2011-10-02 16:45:00 + """, """ + - 2011-10-02 16:45:00 + """) + + def test_iso(self): + round_trip(""" + - 2011-10-02T15:45:00+01:00 + """) + + def test_zero_tz(self): + round_trip(""" + - 2011-10-02T15:45:00+0 + """) + + def test_issue_45(self): + round_trip(""" + dt: 2016-08-19T22:45:47Z + """) diff --git a/constructor.py b/constructor.py index a07a8e3..f2a6cc3 100644 --- a/constructor.py +++ b/constructor.py @@ -1,7 +1,6 @@ # coding: utf-8 -from __future__ import absolute_import -from __future__ import print_function +from __future__ import print_function, absolute_import, division, unicode_literals import collections import datetime @@ -19,7 +18,7 @@ from ruamel.yaml.compat import (utf8, builtins_module, to_str, PY2, PY3, ordereddict, text_type) from ruamel.yaml.comments import * # NOQA from ruamel.yaml.scalarstring import * # NOQA - +from ruamel.yaml.timestamp import TimeStamp __all__ = ['BaseConstructor', 'SafeConstructor', 'Constructor', 'ConstructorError', 'RoundTripConstructor'] @@ -375,7 +374,7 @@ class SafeConstructor(BaseConstructor): u'''^(?P<year>[0-9][0-9][0-9][0-9]) -(?P<month>[0-9][0-9]?) -(?P<day>[0-9][0-9]?) - (?:(?:[Tt]|[ \\t]+) + (?:((?P<t>[Tt])|[ \\t]+) # explictly not retaining extra spaces (?P<hour>[0-9][0-9]?) :(?P<minute>[0-9][0-9]) :(?P<second>[0-9][0-9]) @@ -383,10 +382,10 @@ class SafeConstructor(BaseConstructor): (?:[ \\t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?) (?::(?P<tz_minute>[0-9][0-9]))?))?)?$''', re.X) - def construct_yaml_timestamp(self, node): - value = self.construct_scalar(node) # NOQA - match = self.timestamp_regexp.match(node.value) - values = match.groupdict() + def construct_yaml_timestamp(self, node, values=None): + if values is None: + match = self.timestamp_regexp.match(node.value) + values = match.groupdict() year = int(values['year']) month = int(values['month']) day = int(values['day']) @@ -401,6 +400,8 @@ class SafeConstructor(BaseConstructor): while len(fraction) < 6: fraction += '0' fraction = int(fraction) + if len(values['fraction']) > 6 and int(values['fraction'][6]) > 4: + fraction += 1 delta = None if values['tz_sign']: tz_hour = int(values['tz_hour']) @@ -929,7 +930,6 @@ class RoundTripConstructor(SafeConstructor): index += 1 else: index += 1 - # print ('merge_map_list', merge_map_list) return merge_map_list # if merge: # node.value = merge + node.value @@ -1113,6 +1113,56 @@ class RoundTripConstructor(SafeConstructor): utf8(node.tag), node.start_mark) + def construct_yaml_timestamp(self, node): + match = self.timestamp_regexp.match(node.value) + values = match.groupdict() + if not values['hour']: + return SafeConstructor.construct_yaml_timestamp(self, node, values) + for part in ['t', 'tz_sign', 'tz_hour', 'tz_minute']: + if values[part]: + break + else: + return SafeConstructor.construct_yaml_timestamp(self, node, values) + year = int(values['year']) + month = int(values['month']) + day = int(values['day']) + hour = int(values['hour']) + minute = int(values['minute']) + second = int(values['second']) + fraction = 0 + if values['fraction']: + fraction = values['fraction'][:6] + while len(fraction) < 6: + fraction += '0' + fraction = int(fraction) + if len(values['fraction']) > 6 and int(values['fraction'][6]) > 4: + fraction += 1 + delta = None + if values['tz_sign']: + tz_hour = int(values['tz_hour']) + tz_minute = int(values['tz_minute'] or 0) + delta = datetime.timedelta(hours=tz_hour, minutes=tz_minute) + if values['tz_sign'] == '-': + delta = -delta + if delta: + dt = datetime.datetime(year, month, day, hour, minute) + dt -= delta + data = TimeStamp(dt.year, dt.month, dt.day, dt.hour, dt.minute, + second, fraction) + data._yaml['delta'] = delta + tz = values['tz_sign'] + values['tz_hour'] + if values['tz_minute']: + tz += ':' + values['tz_minute'] + data._yaml['tz'] = tz + else: + data = TimeStamp(year, month, day, hour, minute, second, fraction) + if values['tz']: # no delta + data._yaml['tz'] = values['tz'] + + if values['t']: + data._yaml['t'] = True + return data + RoundTripConstructor.add_constructor( u'tag:yaml.org,2002:null', diff --git a/representer.py b/representer.py index 6129a12..3e67a96 100644 --- a/representer.py +++ b/representer.py @@ -9,6 +9,7 @@ from ruamel.yaml.error import * # NOQA from ruamel.yaml.nodes import * # NOQA from ruamel.yaml.compat import text_type, binary_type, to_unicode, PY2, PY3, ordereddict from ruamel.yaml.scalarstring import * # NOQA +from ruamel.yaml.timestamp import TimeStamp import datetime import sys @@ -851,6 +852,18 @@ class RoundTripRepresenter(SafeRepresenter): tag = u'tag:yaml.org,2002:map' return self.represent_mapping(tag, data) + def represent_datetime(self, data): + inter = 'T' if data._yaml['t'] else ' ' + _yaml = data._yaml + if _yaml['delta']: + data += _yaml['delta'] + value = data.isoformat(inter) + else: + value = data.isoformat(inter) + if _yaml['tz']: + value += _yaml['tz'] + return self.represent_scalar(u'tag:yaml.org,2002:timestamp', to_unicode(value)) + RoundTripRepresenter.add_representer(type(None), RoundTripRepresenter.represent_none) @@ -883,3 +896,6 @@ if sys.version_info >= (2, 7): RoundTripRepresenter.add_representer(CommentedSet, RoundTripRepresenter.represent_set) + +RoundTripRepresenter.add_representer(TimeStamp, + RoundTripRepresenter.represent_datetime) diff --git a/timestamp.py b/timestamp.py new file mode 100644 index 0000000..b0a535c --- /dev/null +++ b/timestamp.py @@ -0,0 +1,13 @@ +# coding: utf-8 + +from __future__ import print_function, absolute_import, division, unicode_literals + +import datetime + + +class TimeStamp(datetime.datetime): + def __init__(self, *args, **kw): + self._yaml = dict(t=False, tz=None, delta=0) + + def __new__(cls, *args, **kw): # datetime is immutable + return datetime.datetime.__new__(cls, *args, **kw) |