Skip to main content

Merge pull request #12 from alexwlchan/append-js-object

ID
92b40d6
date
2024-08-17 20:23:15+00:00
author
Alex Chan <alex@alexwlchan.net>
parents
1ffbd24, 4e143d8
message
Merge pull request #12 from alexwlchan/append-js-object

Add a method for appending a key/value pair to a JSON object
changed files
3 files, 85 additions, 3 deletions

Changed files

README.md (664) → README.md (765)

diff --git a/README.md b/README.md
index c16bab5..194cd13 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,8 @@ Think of this module as the JSON module, but for JavaScript files.
 
 *   You can read a JavaScript file with `read_js(path, varname)`
 *   You can write a JavaScript file with `write_js(path, value, varname)`
-*   You can append an item to a JavaScript array with `append_js_array_value(path, value)`
+*   You can append an item to a JavaScript array with `append_to_js_array(path, value)`
+*   You can append a key-value pair to a JavaScript object with `append_to_js_object(path, key, value)`
 
 ## Installation
 

src/javascript/__init__.py (2984) → src/javascript/__init__.py (4638)

diff --git a/src/javascript/__init__.py b/src/javascript/__init__.py
index b8ebdc4..b0bdd3c 100644
--- a/src/javascript/__init__.py
+++ b/src/javascript/__init__.py
@@ -104,3 +104,54 @@ def append_to_js_array(p: pathlib.Path | str, *, value: typing.Any) -> None:
             return
 
         raise ValueError(f"End of file {p!r} does not look like an array")
+
+
+def append_to_js_object(p: pathlib.Path | str, *, key: str, value: typing.Any) -> None:
+    """
+    Append a single key/value pair to a JSON object in a JavaScript "data file".
+
+    Example:
+
+        >>> write_js('shape.js', value={'colour': 'red', 'sides': 5}, varname='redPentagon')
+        >>> append_to_js_object('shape.js', key='sideLengths', value=[5, 5, 6, 6, 6])
+        >>> read_js('shape.js', varname='redPentagon')
+        {'colour': 'red', 'sides': 5, 'sideLengths': [5, 5, 6, 6, 6]}
+
+    If you have a large file, this is usually faster than reading,
+    appending, and re-writing the entire file.
+
+    """
+    file_size = os.stat(p).st_size
+
+    enc_key = json.dumps(key)
+    enc_value = json.dumps(value)
+
+    json_to_append = f",\n  {enc_key}: {enc_value}\n}};\n".encode("utf8")
+
+    with open(p, "rb+") as out_file:
+        out_file.seek(file_size - 4)
+
+        if out_file.read(4) == b"\n};\n":
+            out_file.seek(file_size - 4)
+            out_file.write(json_to_append)
+            return
+
+        out_file.seek(file_size - 3)
+        if out_file.read(3) in {b"\n};", b"};\n"}:
+            out_file.seek(file_size - 3)
+            out_file.write(json_to_append)
+            return
+
+        out_file.seek(file_size - 2)
+        if out_file.read(2) in {b"};", b"}\n"}:
+            out_file.seek(file_size - 2)
+            out_file.write(json_to_append)
+            return
+
+        out_file.seek(file_size - 1)
+        if out_file.read(1) == b"}":
+            out_file.seek(file_size - 1)
+            out_file.write(json_to_append)
+            return
+
+        raise ValueError(f"End of file {p!r} does not look like an object")

tests/test_javascript.py (5019) → tests/test_javascript.py (6224)

diff --git a/tests/test_javascript.py b/tests/test_javascript.py
index 6ba72e9..a83f910 100644
--- a/tests/test_javascript.py
+++ b/tests/test_javascript.py
@@ -3,7 +3,7 @@ import typing
 
 import pytest
 
-from javascript import append_to_js_array, read_js, write_js
+from javascript import append_to_js_array, append_to_js_object, read_js, write_js
 
 
 @pytest.fixture
@@ -85,7 +85,7 @@ class TestAppendToArray:
             'const fruit = ["apple", "banana", "coconut"];\n',
             'const fruit = ["apple","banana", "coconut"];',
             'const fruit = [\n  "apple",\n  "banana",\n  "coconut"\n];\n',
-            'const fruit = [\n  "apple",\n  "banana",\n  "coconut"\n]',
+            'const fruit = [\n  "apple",\n  "banana",\n  "coconut"\n];',
             'const fruit = [\n  "apple",\n  "banana",\n  "coconut"\n]',
         ],
     )
@@ -119,6 +119,36 @@ class TestAppendToArray:
             append_to_js_array(js_path, value=["yellow"])
 
 
+class TestAppendToObject:
+    @pytest.mark.parametrize(
+        "text",
+        [
+            'const redPentagon = {"colour": "red", "sides": 5};\n',
+            'const redPentagon = {"colour": "red", "sides": 5};',
+            'const redPentagon = {\n  "colour": "red",\n  "sides": 5\n};\n',
+            'const redPentagon = {\n  "colour": "red",\n  "sides": 5\n};',
+            'const redPentagon = {\n  "colour": "red",\n  "sides": 5\n}',
+        ],
+    )
+    def test_can_append_array_value(self, js_path: pathlib.Path, text: str) -> None:
+        js_path.write_text(text)
+
+        append_to_js_object(js_path, key="sideLengths", value=[5, 5, 6, 6, 6])
+        assert read_js(js_path, varname="redPentagon") == {
+            "colour": "red",
+            "sides": 5,
+            "sideLengths": [5, 5, 6, 6, 6],
+        }
+
+    def test_error_if_file_doesnt_look_like_object(self, js_path: pathlib.Path) -> None:
+        shapes = ["apple", "banana", "cherry"]
+
+        write_js(js_path, value=shapes, varname="fruit")
+
+        with pytest.raises(ValueError, match="does not look like an object"):
+            append_to_js_object(js_path, key="sideLengths", value=[5, 5, 6, 6, 6])
+
+
 class TestRoundTrip:
     @pytest.mark.parametrize(
         "value",