changeset 67:563ce5dd02af

I don't care what the YAML spec says, ordered maps are the only sane way.
author Ludovic Chabant <ludovic@chabant.com>
date Fri, 29 Aug 2014 16:42:15 -0700
parents e4a24512b814
children d9e494df2a99 cb1ed436642c
files piecrust/app.py piecrust/configuration.py piecrust/environment.py piecrust/page.py tests/test_configuration.py
diffstat 5 files changed, 55 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/piecrust/app.py	Fri Aug 29 16:41:16 2014 -0700
+++ b/piecrust/app.py	Fri Aug 29 16:42:15 2014 -0700
@@ -4,6 +4,7 @@
 import codecs
 import hashlib
 import logging
+import collections
 import yaml
 from werkzeug.utils import cached_property
 from piecrust import (APP_VERSION,
@@ -15,7 +16,8 @@
 from piecrust.cache import ExtensibleCache, NullCache, NullExtensibleCache
 from piecrust.plugins.base import PluginLoader
 from piecrust.environment import StandardEnvironment
-from piecrust.configuration import Configuration, ConfigurationError, merge_dicts
+from piecrust.configuration import (Configuration, ConfigurationError,
+        OrderedDictYAMLLoader, merge_dicts)
 from piecrust.routing import Route
 from piecrust.sources.base import REALM_USER, REALM_THEME
 from piecrust.taxonomies import Taxonomy
@@ -64,7 +66,8 @@
         if self.cache.isValid('config.json', path_times):
             logger.debug("Loading configuration from cache...")
             config_text = self.cache.read('config.json')
-            self._values = json.loads(config_text)
+            self._values = json.loads(config_text,
+                    object_pairs_hook=collections.OrderedDict)
 
             actual_cache_key = self._values.get('__cache_key')
             if actual_cache_key == cache_key:
@@ -77,7 +80,8 @@
         logger.debug("Loading configuration from: %s" % self.paths)
         for i, p in enumerate(self.paths):
             with codecs.open(p, 'r', 'utf-8') as fp:
-                loaded_values = yaml.load(fp.read())
+                loaded_values = yaml.load(fp.read(),
+                        Loader=OrderedDictYAMLLoader)
             if loaded_values is None:
                 loaded_values = {}
             for fixup in self.fixups:
--- a/piecrust/configuration.py	Fri Aug 29 16:41:16 2014 -0700
+++ b/piecrust/configuration.py	Fri Aug 29 16:42:15 2014 -0700
@@ -1,6 +1,8 @@
 import re
 import logging
+import collections
 import yaml
+from yaml.constructor import ConstructorError
 
 
 logger = logging.getLogger(__name__)
@@ -114,10 +116,29 @@
     m = header_regex.match(text)
     if m is not None:
         header = str(m.group('header'))
-        config = yaml.load(header, Loader=yaml.BaseLoader)
+        config = yaml.load(header, Loader=OrderedDictYAMLLoader)
         offset = m.end()
     else:
         config = {}
         offset = 0
     return config, offset
 
+
+class OrderedDictYAMLLoader(yaml.BaseLoader):
+    """ A YAML loader that loads mappings into ordered dictionaries.
+    """
+    def construct_mapping(self, node, deep=False):
+        if not isinstance(node, yaml.MappingNode):
+            raise ConstructorError(None, None,
+                    "expected a mapping node, but found %s" % node.id,
+                    node.start_mark)
+        mapping = collections.OrderedDict()
+        for key_node, value_node in node.value:
+            key = self.construct_object(key_node, deep=deep)
+            if not isinstance(key, collections.Hashable):
+                raise ConstructorError("while constructing a mapping", node.start_mark,
+                        "found unhashable key", key_node.start_mark)
+            value = self.construct_object(value_node, deep=deep)
+            mapping[key] = value
+        return mapping
+
--- a/piecrust/environment.py	Fri Aug 29 16:41:16 2014 -0700
+++ b/piecrust/environment.py	Fri Aug 29 16:42:15 2014 -0700
@@ -3,6 +3,7 @@
 import json
 import logging
 import threading
+import collections
 import repoze.lru
 
 
@@ -40,7 +41,8 @@
                             logger.debug("'%s' found in file-system cache." %
                                          key)
                             item_raw = self.fs_cache.read(fs_key)
-                            item = json.loads(item_raw)
+                            item = json.loads(item_raw,
+                                    object_pairs_hook=collections.OrderedDict)
                             self.cache.put(key, item)
                             return item
 
--- a/piecrust/page.py	Fri Aug 29 16:41:16 2014 -0700
+++ b/piecrust/page.py	Fri Aug 29 16:42:15 2014 -0700
@@ -7,6 +7,7 @@
 import logging
 import datetime
 import dateutil.parser
+import collections
 from werkzeug.utils import cached_property
 from piecrust.configuration import (Configuration, ConfigurationError,
         parse_config_header)
@@ -184,7 +185,8 @@
     page_time = path_mtime or os.path.getmtime(path)
     if cache.isValid(cache_path, page_time):
         exec_info.was_cache_valid = True
-        cache_data = json.loads(cache.read(cache_path))
+        cache_data = json.loads(cache.read(cache_path),
+                object_pairs_hook=collections.OrderedDict)
         config = PageConfiguration(values=cache_data['config'],
                 validate=False)
         content = json_load_segments(cache_data['content'])
--- a/tests/test_configuration.py	Fri Aug 29 16:41:16 2014 -0700
+++ b/tests/test_configuration.py	Fri Aug 29 16:42:15 2014 -0700
@@ -1,6 +1,9 @@
 import copy
+import yaml
 import pytest
-from piecrust.configuration import Configuration, merge_dicts
+from collections import OrderedDict
+from piecrust.configuration import (Configuration, OrderedDictYAMLLoader,
+        merge_dicts)
 
 
 @pytest.mark.parametrize('values, expected', [
@@ -103,3 +106,19 @@
             }
     assert config.get() == expected
 
+
+def test_ordered_loader():
+    sample = """
+one:
+    two: fish
+    red: fish
+    blue: fish
+two:
+    a: yes
+    b: no
+    c: null
+"""
+    data = yaml.load(sample, Loader=OrderedDictYAMLLoader)
+    assert type(data) is OrderedDict
+    assert list(data['one'].keys()) == ['two', 'red', 'blue']
+