summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst9
-rw-r--r--_test/roundtrip.py4
-rw-r--r--_test/test_datetime.py121
-rw-r--r--constructor.py68
-rw-r--r--representer.py16
-rw-r--r--timestamp.py13
6 files changed, 222 insertions, 9 deletions
diff --git a/README.rst b/README.rst
index 1be8309..b0574ec 100644
--- a/README.rst
+++ b/README.rst
@@ -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)