# HG changeset patch # User Ludovic Chabant # Date 1413439265 25200 # Node ID 10fc9c8bf682288640a532d72150389e54efb860 # Parent 5effaf1978d03a823fd70ca1ef25962dc95789d1 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. diff -r 5effaf1978d0 -r 10fc9c8bf682 piecrust/app.py --- 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: diff -r 5effaf1978d0 -r 10fc9c8bf682 piecrust/commands/builtin/info.py --- 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) diff -r 5effaf1978d0 -r 10fc9c8bf682 piecrust/configuration.py --- 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[0-9][0-9]?) + :(?P[0-9][0-9]) + (:(?P[0-9][0-9]) + (\.(?P[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) + diff -r 5effaf1978d0 -r 10fc9c8bf682 tests/test_configuration.py --- 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) +