Repository: sdispater/tomlkit (python)

Issue:
Serializing OrderedDict (and dict in Python 3) does not preserve order
The following is the case in both Python 2.7 and Python 3.x.
```
>>> import tomlkit, json, collections
>>> json.dumps(collections.OrderedDict([('foo', 1), ('bar', 2), ('baz', 3)]))
'{"foo": 1, "bar": 2, "baz": 3}'
>>> tomlkit.dumps(collections.OrderedDict([('foo', 1), ('bar', 2), ('baz', 3)]))
'bar = 2\nbaz = 3\nfoo = 1\n'
```

Pull request description:
Fix the default ordering of dicts and add support for sorting keys
Resolves #93 

This PR fixes an issue where the order of the original dict was not preserved when dumping it.

It also adds the ability to tell TOMLKit to sort keys when dumping by passing `sort_keys=True` to `dumps()`.

Patch:
diff --git a/tomlkit/_compat.py b/tomlkit/_compat.py
index d5cb1f8..8d3b0ae 100644
--- a/tomlkit/_compat.py
+++ b/tomlkit/_compat.py
@@ -150,6 +150,12 @@ else:
     long = int
 
 
+if PY36:
+    OrderedDict = dict
+else:
+    from collections import OrderedDict
+
+
 def decode(string, encodings=None):
     if not PY2 and not isinstance(string, bytes):
         return string
diff --git a/tomlkit/api.py b/tomlkit/api.py
index 3f47526..3de4121 100644
--- a/tomlkit/api.py
+++ b/tomlkit/api.py
@@ -34,12 +34,12 @@ def loads(string):  # type: (str) -> _TOMLDocument
     return parse(string)
 
 
-def dumps(data):  # type: (_TOMLDocument) -> str
+def dumps(data, sort_keys=False):  # type: (_TOMLDocument, bool) -> str
     """
     Dumps a TOMLDocument into a string.
     """
     if not isinstance(data, _TOMLDocument) and isinstance(data, dict):
-        data = item(data)
+        data = item(data, _sort_keys=sort_keys)
 
     return data.as_string()
 
diff --git a/tomlkit/items.py b/tomlkit/items.py
index 8218f9c..184ffe7 100644
--- a/tomlkit/items.py
+++ b/tomlkit/items.py
@@ -28,7 +28,7 @@ else:
     from functools import lru_cache
 
 
-def item(value, _parent=None):
+def item(value, _parent=None, _sort_keys=False):
     from .container import Container
 
     if isinstance(value, Item):
@@ -42,8 +42,11 @@ def item(value, _parent=None):
         return Float(value, Trivia(), str(value))
     elif isinstance(value, dict):
         val = Table(Container(), Trivia(), False)
-        for k, v in sorted(value.items(), key=lambda i: (isinstance(i[1], dict), i[0])):
-            val[k] = item(v, _parent=val)
+        for k, v in sorted(
+            value.items(),
+            key=lambda i: (isinstance(i[1], dict), i[0] if _sort_keys else 1),
+        ):
+            val[k] = item(v, _parent=val, _sort_keys=_sort_keys)
 
         return val
     elif isinstance(value, list):
@@ -57,13 +60,14 @@ def item(value, _parent=None):
                 table = Table(Container(), Trivia(), True)
 
                 for k, _v in sorted(
-                    v.items(), key=lambda i: (isinstance(i[1], dict), i[0])
+                    v.items(),
+                    key=lambda i: (isinstance(i[1], dict), i[0] if _sort_keys else 1),
                 ):
-                    i = item(_v)
+                    i = item(_v, _sort_keys=_sort_keys)
                     if isinstance(table, InlineTable):
                         i.trivia.trail = ""
 
-                    table[k] = item(i)
+                    table[k] = item(i, _sort_keys=_sort_keys)
 
                 v = table
 


Test patch:
diff --git a/tests/test_items.py b/tests/test_items.py
index c4b074c..5fc30de 100644
--- a/tests/test_items.py
+++ b/tests/test_items.py
@@ -14,6 +14,7 @@ import pytest
 from tomlkit import inline_table
 from tomlkit import parse
 from tomlkit._compat import PY2
+from tomlkit._compat import OrderedDict
 from tomlkit.exceptions import NonExistentKey
 from tomlkit.items import Bool
 from tomlkit.items import InlineTable
@@ -208,8 +209,56 @@ bar = "baz"
     )
 
 
-def test_dicts_are_converted_to_tables_and_sorted():
-    t = item({"foo": {"bar": "baz", "abc": 123, "baz": [{"c": 3, "b": 2, "a": 1}]}})
+def test_dicts_are_converted_to_tables_and_keep_order():
+    t = item(
+        OrderedDict(
+            [
+                (
+                    "foo",
+                    OrderedDict(
+                        [
+                            ("bar", "baz"),
+                            ("abc", 123),
+                            ("baz", [OrderedDict([("c", 3), ("b", 2), ("a", 1)])]),
+                        ]
+                    ),
+                )
+            ]
+        )
+    )
+
+    assert (
+        t.as_string()
+        == """[foo]
+bar = "baz"
+abc = 123
+
+[[foo.baz]]
+c = 3
+b = 2
+a = 1
+"""
+    )
+
+
+def test_dicts_are_converted_to_tables_and_are_sorted_if_requested():
+    t = item(
+        OrderedDict(
+            [
+                (
+                    "foo",
+                    OrderedDict(
+                        [
+                            ("bar", "baz"),
+                            ("abc", 123),
+                            ("baz", [OrderedDict([("c", 3), ("b", 2), ("a", 1)])]),
+                        ]
+                    ),
+                )
+            ]
+        ),
+        _sort_keys=True,
+    )
 
     assert (
         t.as_string()
@@ -226,7 +275,9 @@ c = 3
 
 
 def test_dicts_with_sub_dicts_are_properly_converted():
-    t = item({"foo": {"bar": {"string": "baz"}, "int": 34, "float": 3.14}})
+    t = item(
+        {"foo": {"bar": {"string": "baz"}, "int": 34, "float": 3.14}}, _sort_keys=True
+    )
 
     assert (
         t.as_string()