Skip to main content

Merge pull request #33 from alexwlchan/sort-keys

ID
aa4dce1
date
2025-05-05 07:37:07+00:00
author
Alex Chan <alex@alexwlchan.net>
parents
d991f3a, 059afac
message
Merge pull request #33 from alexwlchan/sort-keys

Add a `sort_keys` parameter to `write_js`
changed files
5 files, 60 additions, 12 deletions

Changed files

CHANGELOG.md (3923) → CHANGELOG.md (4074)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a9a80c0..c64ee04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # CHANGELOG
 
+## v1.3.0 - 2025-05-05
+
+Add a `sort_keys` parameter to `write_js`.
+If `True`, dictionaries with be serialised to JSON sorted by key.
+Default `False`.
+
 ## v1.2.3 - 2025-05-04
 
 Tweak the error message introduced in v1.2.2 -- JSON objects are **name**/value pairs, not key/value pairs.

src/javascript_data_files/__init__.py (6783) → src/javascript_data_files/__init__.py (6833)

diff --git a/src/javascript_data_files/__init__.py b/src/javascript_data_files/__init__.py
index 2f72ea2..bbdecc4 100644
--- a/src/javascript_data_files/__init__.py
+++ b/src/javascript_data_files/__init__.py
@@ -74,6 +74,7 @@ def write_js(
     *,
     value: typing.Any,
     varname: str,
+    sort_keys: bool = False,
 ) -> None:
     """
     Write a JavaScript "data file".
@@ -88,7 +89,7 @@ def write_js(
         'const redPentagon = {\n  "sides": 5,\n  "colour": "red"\n};\n'
 
     """
-    js_string = encode_as_js(value, varname)
+    js_string = encode_as_js(value, varname, sort_keys=sort_keys)
 
     if isinstance(p, io.TextIOBase):
         p.write(js_string)

src/javascript_data_files/encoder.py (1155) → src/javascript_data_files/encoder.py (1253)

diff --git a/src/javascript_data_files/encoder.py b/src/javascript_data_files/encoder.py
index 6720d82..3f5087d 100644
--- a/src/javascript_data_files/encoder.py
+++ b/src/javascript_data_files/encoder.py
@@ -26,18 +26,18 @@ class HumanReadableEncoder(json.JSONEncoder):
         return super().encode(o)
 
 
-def encode_as_json(value: typing.Any) -> str:
+def encode_as_json(value: typing.Any, *, sort_keys: bool = False) -> str:
     """
     Convert a Python value to a JSON-encoded string.
     """
-    return json.dumps(value, indent=2, cls=HumanReadableEncoder)
+    return json.dumps(value, indent=2, sort_keys=sort_keys, cls=HumanReadableEncoder)
 
 
-def encode_as_js(value: typing.Any, varname: str) -> str:
+def encode_as_js(value: typing.Any, varname: str, *, sort_keys: bool = False) -> str:
     """
     Convert a Python value to a JSON-encoded JavaScript value.
     """
-    json_string = encode_as_json(value)
+    json_string = encode_as_json(value, sort_keys=sort_keys)
     js_string = f"const {varname} = {json_string};\n"
 
     return js_string

tests/test_encoder.py (2541) → tests/test_encoder.py (2946)

diff --git a/tests/test_encoder.py b/tests/test_encoder.py
index 16163ca..e139915 100644
--- a/tests/test_encoder.py
+++ b/tests/test_encoder.py
@@ -17,6 +17,21 @@ def test_it_pretty_prints_json() -> None:
     )
 
 
+def test_it_sorts_keys() -> None:
+    """
+    If you pass `sort_keys=True`, it sorts the keys in JSON objects.
+    """
+    assert (
+        encode_as_json({"sides": 5, "colour": "red"}, sort_keys=False)
+        == '{\n  "sides": 5,\n  "colour": "red"\n}'
+    )
+
+    assert (
+        encode_as_json({"sides": 5, "colour": "red"}, sort_keys=True)
+        == '{\n  "colour": "red",\n  "sides": 5\n}'
+    )
+
+
 def test_a_list_of_ints_is_not_split_over_multiple_lines() -> None:
     """
     If there's a list of small integers, they're printed on one line

tests/test_javascript_data_files.py (17712) → tests/test_javascript_data_files.py (18531)

diff --git a/tests/test_javascript_data_files.py b/tests/test_javascript_data_files.py
index 3446a02..79553a0 100644
--- a/tests/test_javascript_data_files.py
+++ b/tests/test_javascript_data_files.py
@@ -153,7 +153,7 @@ class TestWriteJs:
     Tests for the ``write_js()`` function.
     """
 
-    def test_can_write_file(self, js_path: pathlib.Path) -> None:
+    def test_write_file(self, js_path: pathlib.Path) -> None:
         """
         Writing to a file stores the correct JavaScript string.
         """
@@ -166,7 +166,7 @@ class TestWriteJs:
             == 'const redPentagon = {\n  "sides": 5,\n  "colour": "red"\n};\n'
         )
 
-    def test_can_write_to_str(self, tmp_path: pathlib.Path) -> None:
+    def test_write_to_str(self, tmp_path: pathlib.Path) -> None:
         """
         It can write to a path passed as a ``str``.
         """
@@ -181,7 +181,7 @@ class TestWriteJs:
             == 'const redPentagon = {\n  "sides": 5,\n  "colour": "red"\n};\n'
         )
 
-    def test_can_write_to_path(self, tmp_path: pathlib.Path) -> None:
+    def test_write_to_path(self, tmp_path: pathlib.Path) -> None:
         """
         It can write to a path passed as a ``pathlib.Path``.
         """
@@ -196,7 +196,7 @@ class TestWriteJs:
             == 'const redPentagon = {\n  "sides": 5,\n  "colour": "red"\n};\n'
         )
 
-    def test_can_write_to_file(self, tmp_path: pathlib.Path) -> None:
+    def test_write_to_file(self, tmp_path: pathlib.Path) -> None:
         """
         It can write to a file.
         """
@@ -212,7 +212,7 @@ class TestWriteJs:
             == 'const redPentagon = {\n  "sides": 5,\n  "colour": "red"\n};\n'
         )
 
-    def test_can_write_to_binary_file(self, tmp_path: pathlib.Path) -> None:
+    def test_write_to_binary_file(self, tmp_path: pathlib.Path) -> None:
         """
         It can write to a file opened in binary mode.
         """
@@ -228,7 +228,7 @@ class TestWriteJs:
             == 'const redPentagon = {\n  "sides": 5,\n  "colour": "red"\n};\n'
         )
 
-    def test_can_write_to_string_buffer(self) -> None:
+    def test_write_to_string_buffer(self) -> None:
         """
         It can write to a string buffer.
         """
@@ -243,7 +243,7 @@ class TestWriteJs:
             == 'const redPentagon = {\n  "sides": 5,\n  "colour": "red"\n};\n'
         )
 
-    def test_can_write_to_bytes_buffer(self) -> None:
+    def test_write_to_bytes_buffer(self) -> None:
         """
         It can write to a binary buffer.
         """
@@ -258,6 +258,32 @@ class TestWriteJs:
             == b'const redPentagon = {\n  "sides": 5,\n  "colour": "red"\n};\n'
         )
 
+    def test_write_with_sort_keys(self, tmp_path: pathlib.Path) -> None:
+        """
+        If you pass `sort_keys=True`, it sorts the keys in JSON objects.
+        """
+        red_pentagon = {"sides": 5, "colour": "red"}
+
+        unsorted_path = tmp_path / "unsorted.js"
+        sorted_path = tmp_path / "sorted.js"
+
+        write_js(
+            unsorted_path,
+            value=red_pentagon,
+            varname="redPentagon",
+            sort_keys=False,
+        )
+        assert (
+            unsorted_path.read_text()
+            == 'const redPentagon = {\n  "sides": 5,\n  "colour": "red"\n};\n'
+        )
+
+        write_js(sorted_path, value=red_pentagon, varname="redPentagon", sort_keys=True)
+        assert (
+            sorted_path.read_text()
+            == 'const redPentagon = {\n  "colour": "red",\n  "sides": 5\n};\n'
+        )
+
     def test_fails_if_file_is_read_only(self, tmp_path: pathlib.Path) -> None:
         """
         It cannot write to a file open in read-only mode.