changeset 107:10fc9c8bf682

Better support for times in YAML interop. * Use our own sexagesimal parser/dumper for YAML to properly parse times. * Better name for the custom parser/dumper classes. * Add unit tests.
author Ludovic Chabant <ludovic@chabant.com>
date Wed, 15 Oct 2014 23:01:05 -0700
parents 5effaf1978d0
children b6ec402d32bb
files piecrust/app.py piecrust/commands/builtin/info.py piecrust/configuration.py tests/test_configuration.py
diffstat 4 files changed, 79 insertions(+), 10 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/app.py	Wed Oct 15 21:18:27 2014 -0700
+++ b/piecrust/app.py	Wed Oct 15 23:01:05 2014 -0700
@@ -17,7 +17,7 @@
 from piecrust.plugins.base import PluginLoader
 from piecrust.environment import StandardEnvironment
 from piecrust.configuration import (Configuration, ConfigurationError,
-        OrderedDictYAMLLoader, merge_dicts)
+        ConfigurationLoader, merge_dicts)
 from piecrust.routing import Route
 from piecrust.sources.base import REALM_USER, REALM_THEME
 from piecrust.taxonomies import Taxonomy
@@ -81,7 +81,7 @@
         for i, p in enumerate(self.paths):
             with codecs.open(p, 'r', 'utf-8') as fp:
                 loaded_values = yaml.load(fp.read(),
-                        Loader=OrderedDictYAMLLoader)
+                        Loader=ConfigurationLoader)
             if loaded_values is None:
                 loaded_values = {}
             for fixup in self.fixups:
--- a/piecrust/commands/builtin/info.py	Wed Oct 15 21:18:27 2014 -0700
+++ b/piecrust/commands/builtin/info.py	Wed Oct 15 23:01:05 2014 -0700
@@ -2,6 +2,7 @@
 import logging
 import fnmatch
 from piecrust.commands.base import ChefCommand
+from piecrust.configuration import ConfigurationDumper
 
 
 logger = logging.getLogger(__name__)
@@ -36,7 +37,8 @@
         if show is not None:
             if isinstance(show, (dict, list)):
                 import yaml
-                out = yaml.safe_dump(show, default_flow_style=False)
+                out = yaml.dump(show, default_flow_style=False,
+                                Dumper=ConfigurationDumper)
                 logger.info(out)
             else:
                 logger.info(show)
--- a/piecrust/configuration.py	Wed Oct 15 21:18:27 2014 -0700
+++ b/piecrust/configuration.py	Wed Oct 15 23:01:05 2014 -0700
@@ -116,7 +116,7 @@
     m = header_regex.match(text)
     if m is not None:
         header = str(m.group('header'))
-        config = yaml.load(header, Loader=OrderedDictYAMLLoader)
+        config = yaml.load(header, Loader=ConfigurationLoader)
         offset = m.end()
     else:
         config = {}
@@ -124,16 +124,18 @@
     return config, offset
 
 
-class OrderedDictYAMLLoader(yaml.SafeLoader):
+class ConfigurationLoader(yaml.SafeLoader):
     """ A YAML loader that loads mappings into ordered dictionaries.
     """
     def __init__(self, *args, **kwargs):
-        super(OrderedDictYAMLLoader, self).__init__(*args, **kwargs)
+        super(ConfigurationLoader, self).__init__(*args, **kwargs)
 
-        self.add_constructor(u'tag:yaml.org,2002:map',
+        self.add_constructor('tag:yaml.org,2002:map',
                 type(self).construct_yaml_map)
-        self.add_constructor(u'tag:yaml.org,2002:omap',
+        self.add_constructor('tag:yaml.org,2002:omap',
                 type(self).construct_yaml_map)
+        self.add_constructor('tag:yaml.org,2002:sexagesimal',
+                type(self).construct_yaml_time)
 
     def construct_yaml_map(self, node):
         data = collections.OrderedDict()
@@ -156,3 +158,49 @@
             mapping[key] = value
         return mapping
 
+    time_regexp = re.compile(
+            r'''^(?P<hour>[0-9][0-9]?)
+                :(?P<minute>[0-9][0-9])
+                (:(?P<second>[0-9][0-9])
+                (\.(?P<fraction>[0-9]+))?)?$''', re.X)
+
+    def construct_yaml_time(self, node):
+        self.construct_scalar(node)
+        match = self.time_regexp.match(node.value)
+        values = match.groupdict()
+        hour = int(values['hour'])
+        minute = int(values['minute'])
+        second = 0
+        if values['second']:
+            second = int(values['second'])
+        usec = 0
+        if values['fraction']:
+            usec = float('0.' + values['fraction'])
+        return second + minute * 60 + hour * 60 * 60 + usec
+
+
+ConfigurationLoader.add_implicit_resolver(
+        'tag:yaml.org,2002:sexagesimal',
+        re.compile(r'''^[0-9][0-9]?:[0-9][0-9]
+                    (:[0-9][0-9](\.[0-9]+)?)?$''', re.X),
+        list('0123456789'))
+
+
+# We need to add our `sexagesimal` resolver before the `int` one, which
+# already supports sexagesimal notation in YAML 1.1 (but not 1.2). However,
+# because we know we pretty much always want it for representing time, we
+# need a simple `12:30` to mean 45000, not 750. So that's why we override
+# the default behaviour.
+for ch in list('0123456789'):
+    ch_resolvers = ConfigurationLoader.yaml_implicit_resolvers[ch]
+    ch_resolvers.insert(0, ch_resolvers.pop())
+
+
+class ConfigurationDumper(yaml.SafeDumper):
+    def represent_ordered_dict(self, data):
+        return self.represent_mapping('tag:yaml.org,2002:omap', data)
+
+
+ConfigurationDumper.add_representer(collections.OrderedDict,
+        ConfigurationDumper.represent_ordered_dict)
+
--- a/tests/test_configuration.py	Wed Oct 15 21:18:27 2014 -0700
+++ b/tests/test_configuration.py	Wed Oct 15 23:01:05 2014 -0700
@@ -1,8 +1,9 @@
 import copy
+import datetime
 import yaml
 import pytest
 from collections import OrderedDict
-from piecrust.configuration import (Configuration, OrderedDictYAMLLoader,
+from piecrust.configuration import (Configuration, ConfigurationLoader,
         merge_dicts)
 
 
@@ -118,7 +119,25 @@
     b: no
     c: null
 """
-    data = yaml.load(sample, Loader=OrderedDictYAMLLoader)
+    data = yaml.load(sample, Loader=ConfigurationLoader)
     assert type(data) is OrderedDict
     assert list(data['one'].keys()) == ['two', 'red', 'blue']
 
+
+def test_load_time1():
+    sample = """
+time: 21:35
+"""
+    data = yaml.load(sample, Loader=ConfigurationLoader)
+    assert type(data['time']) is int
+    assert data['time'] == (21 * 60 * 60 + 35 * 60)
+
+
+def test_load_time2():
+    sample = """
+time: 21:35:50
+"""
+    data = yaml.load(sample, Loader=ConfigurationLoader)
+    assert type(data['time']) is int
+    assert data['time'] == (21 * 60 * 60 + 35 * 60 + 50)
+