Skip to main content

Merge pull request #17 from alexwlchan/stricter-tests

ID
dac85ff
date
2024-08-24 09:27:24+00:00
author
Alex Chan <alex@alexwlchan.net>
parents
12c9255, 933311f
message
Merge pull request #17 from alexwlchan/stricter-tests

Add more tests and test documentation
changed files
6 files, 186 additions, 5 deletions

Changed files

.github/workflows/test.yml (616) → .github/workflows/test.yml (673)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 754e0a6..afd767e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -29,6 +29,9 @@ jobs:
         ruff check .
         ruff format --check .
 
+    - name: Check docstrings
+      run: interrogate -vv
+
     - name: Check types
       run: mypy src tests
     - name: Run tests

dev_requirements.in (44) → dev_requirements.in (56)

diff --git a/dev_requirements.in b/dev_requirements.in
index 4198c39..1d9fecc 100644
--- a/dev_requirements.in
+++ b/dev_requirements.in
@@ -1,6 +1,7 @@
 -e file:.
 
 build
+interrogate
 mypy
 pytest-cov
 ruff

dev_requirements.txt (1669) → dev_requirements.txt (1901)

diff --git a/dev_requirements.txt b/dev_requirements.txt
index 4e2a18c..c9563e3 100644
--- a/dev_requirements.txt
+++ b/dev_requirements.txt
@@ -2,12 +2,18 @@
 #    uv pip compile dev_requirements.in --output-file dev_requirements.txt
 -e file:.
     # via -r dev_requirements.in
+attrs==24.2.0
+    # via interrogate
 build==1.2.1
     # via -r dev_requirements.in
 certifi==2024.7.4
     # via requests
 charset-normalizer==3.3.2
     # via requests
+click==8.1.7
+    # via interrogate
+colorama==0.4.6
+    # via interrogate
 coverage==7.6.1
     # via pytest-cov
 docutils==0.21.2
@@ -18,6 +24,8 @@ importlib-metadata==8.2.0
     # via twine
 iniconfig==2.0.0
     # via pytest
+interrogate==1.7.0
+    # via -r dev_requirements.in
 jaraco-classes==3.4.0
     # via keyring
 jaraco-context==5.3.0
@@ -48,6 +56,8 @@ pkginfo==1.10.0
     # via twine
 pluggy==1.5.0
     # via pytest
+py==1.11.0
+    # via interrogate
 pygments==2.18.0
     # via
     #   readme-renderer
@@ -72,6 +82,8 @@ rich==13.7.1
     # via twine
 ruff==0.6.1
     # via -r dev_requirements.in
+tabulate==0.9.0
+    # via interrogate
 twine==5.1.1
     # via -r dev_requirements.in
 typing-extensions==4.12.2

pyproject.toml (1189) → pyproject.toml (1252)

diff --git a/pyproject.toml b/pyproject.toml
index 19b123b..d3aecf0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -51,3 +51,7 @@ filterwarnings = ["error"]
 [tool.mypy]
 mypy_path = "src"
 strict = true
+
+[tool.interrogate]
+fail_under = 100
+omit-covered-files = true

src/javascript/__init__.py (4632) → src/javascript/__init__.py (5019)

diff --git a/src/javascript/__init__.py b/src/javascript/__init__.py
index aef8e55..36ec47f 100644
--- a/src/javascript/__init__.py
+++ b/src/javascript/__init__.py
@@ -1,3 +1,16 @@
+"""
+This is a collection of Python functions for manipulating JavaScript
+"data files" -- that is, JavaScript files that define a single variable
+with a JSON value.
+
+This is an example of a JavaScript data file:
+
+    const shape = { "sides": 5, "colour": "red" };
+
+Think of this like the JSON module, but for JavaScript files.
+
+"""
+
 import json
 import pathlib
 import re
@@ -51,6 +64,9 @@ def write_js(p: pathlib.Path | str, *, value: typing.Any, varname: str) -> None:
     """
     p = pathlib.Path(p)
 
+    if p.is_dir():
+        raise IsADirectoryError(p)
+
     json_string = json.dumps(value, indent=2)
     js_string = f"const {varname} = {json_string};\n"
 

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

diff --git a/tests/test_javascript.py b/tests/test_javascript.py
index a83f910..afad739 100644
--- a/tests/test_javascript.py
+++ b/tests/test_javascript.py
@@ -1,3 +1,7 @@
+"""
+Tests for the ``javascript`` module.
+"""
+
 import pathlib
 import typing
 
@@ -8,10 +12,19 @@ from javascript import append_to_js_array, append_to_js_object, read_js, write_j
 
 @pytest.fixture
 def js_path(tmp_path: pathlib.Path) -> pathlib.Path:
+    """
+    Returns a path to a JavaScript file.
+
+    This only returns the path and does not create the file.
+    """
     return tmp_path / "data.js"
 
 
 class TestReadJs:
+    """
+    Tests for the ``read_js()`` function.
+    """
+
     @pytest.mark.parametrize(
         "text",
         [
@@ -23,21 +36,49 @@ class TestReadJs:
         ],
     )
     def test_can_read_file(self, js_path: pathlib.Path, text: str) -> None:
+        """
+        JavaScript "data values" can be read from files, with a certain
+        amount of allowance for:
+
+        *   whitespace
+        *   trailing semicolon or not
+        *   a var/const prefix
+
+        """
         js_path.write_text(text)
 
         assert read_js(js_path, varname="redPentagon") == {"sides": 5, "colour": "red"}
 
-    def test_non_existent_file_is_error(self) -> None:
+    def test_error_if_path_does_not_exist(self) -> None:
+        """
+        Reading a file which doesn't exist throws a FileNotFoundError.
+        """
         with pytest.raises(FileNotFoundError):
             read_js("doesnotexist.js", varname="shape")
 
+    def test_error_if_path_is_directory(self, tmp_path: pathlib.Path) -> None:
+        """
+        Reading a path which is a directory throws an IsADirectoryError.
+        """
+        assert tmp_path.is_dir()
+
+        with pytest.raises(IsADirectoryError):
+            read_js(tmp_path, varname="shape")
+
     def test_non_json_value_is_error(self, js_path: pathlib.Path) -> None:
+        """
+        Reading a file which doesn't contain a JavaScript "data value"
+        throws a ValueError.
+        """
         js_path.write_text("const sum = 1 + 1 + 1;")
 
         with pytest.raises(ValueError):
             read_js(js_path, varname="sum")
 
     def test_incorrect_varname_is_error(self, js_path: pathlib.Path) -> None:
+        """
+        Reading a file with the wrong variable name throws a ValueError.
+        """
         js_path.write_text(
             'const redPentagon = {\n  "sides": 5,\n  "colour": "red"\n};\n'
         )
@@ -49,7 +90,14 @@ class TestReadJs:
 
 
 class TestWriteJs:
+    """
+    Tests for the ``write_js()`` function.
+    """
+
     def test_can_write_file(self, js_path: pathlib.Path) -> None:
+        """
+        Writing to a file stores the correct JavaScript string.
+        """
         red_pentagon = {"sides": 5, "colour": "red"}
 
         write_js(js_path, value=red_pentagon, varname="redPentagon")
@@ -60,12 +108,30 @@ class TestWriteJs:
         )
 
     def test_fails_if_cannot_write_file(self) -> None:
+        """
+        Writing to the root folder throws an IsADirectoryError.
+        """
         red_pentagon = {"sides": 5, "colour": "red"}
 
-        with pytest.raises(OSError):
+        with pytest.raises(IsADirectoryError):
             write_js("/", value=red_pentagon, varname="redPentagon")
 
+    def test_fails_if_target_is_folder(self, tmp_path: pathlib.Path) -> None:
+        """
+        Writing to a folder throws an IsADirectoryError.
+        """
+        assert tmp_path.is_dir()
+
+        red_pentagon = {"sides": 5, "colour": "red"}
+
+        with pytest.raises(IsADirectoryError):
+            write_js(tmp_path, value=red_pentagon, varname="redPentagon")
+
     def test_creates_parent_directory(self, tmp_path: pathlib.Path) -> None:
+        """
+        If the parent directory of the output path doesn't exist, it is
+        created before the file is written.
+        """
         js_path = tmp_path / "1/2/3/shape.js"
         red_pentagon = {"sides": 5, "colour": "red"}
 
@@ -79,6 +145,10 @@ class TestWriteJs:
 
 
 class TestAppendToArray:
+    """
+    Tests for the ``append_to_js_array`` function.
+    """
+
     @pytest.mark.parametrize(
         "text",
         [
@@ -90,6 +160,13 @@ class TestAppendToArray:
         ],
     )
     def test_can_append_array_value(self, js_path: pathlib.Path, text: str) -> None:
+        """
+        After you append an item to an array, you can retrieve the
+        updated array.
+
+        This is true regardless of what the trailing whitespace in the
+        original file looks like.
+        """
         js_path.write_text(text)
 
         append_to_js_array(js_path, value="damson")
@@ -101,6 +178,9 @@ class TestAppendToArray:
         ]
 
     def test_can_mix_types(self, js_path: pathlib.Path) -> None:
+        """
+        Arrays can contain a mixture of different types.
+        """
         write_js(js_path, value=["apple", "banana", "coconut"], varname="fruit")
         append_to_js_array(js_path, value=["damson"])
         assert read_js(js_path, varname="fruit") == [
@@ -111,6 +191,10 @@ class TestAppendToArray:
         ]
 
     def test_error_if_file_doesnt_look_like_array(self, js_path: pathlib.Path) -> None:
+        """
+        Appending to a file which doesn't contain a JSON array throws
+        a ValueError.
+        """
         red_pentagon = {"sides": 5, "colour": "red"}
 
         write_js(js_path, value=red_pentagon, varname="redPentagon")
@@ -118,8 +202,26 @@ class TestAppendToArray:
         with pytest.raises(ValueError, match="does not look like an array"):
             append_to_js_array(js_path, value=["yellow"])
 
+    def test_error_if_path_doesnt_exist(self, js_path: pathlib.Path) -> None:
+        """
+        Appending to the path of a non-existent file throws FileNotFoundError.
+        """
+        with pytest.raises(FileNotFoundError):
+            append_to_js_array(js_path, value="alex")
+
+    def test_error_if_path_is_dir(self, tmp_path: pathlib.Path) -> None:
+        """
+        Appending to a path which is a directory throws IsADirectoryError.
+        """
+        with pytest.raises(IsADirectoryError):
+            append_to_js_array(tmp_path, value="alex")
+
 
 class TestAppendToObject:
+    """
+    Tests for the ``append_to_js_object`` function.
+    """
+
     @pytest.mark.parametrize(
         "text",
         [
@@ -130,17 +232,30 @@ class TestAppendToObject:
             'const redPentagon = {\n  "colour": "red",\n  "sides": 5\n}',
         ],
     )
-    def test_can_append_array_value(self, js_path: pathlib.Path, text: str) -> None:
+    def test_append_to_js_object(self, js_path: pathlib.Path, text: str) -> None:
+        """
+        After you add a key/value pair to an object, you can retrieve the
+        updated object.
+
+        This is true regardless of what the trailing whitespace in the
+        original file looks like.
+        """
         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}
+
+        append_to_js_object(js_path, key="sideLengths", value=[1, 2, 3, 4, 5])
         assert read_js(js_path, varname="redPentagon") == {
             "colour": "red",
             "sides": 5,
-            "sideLengths": [5, 5, 6, 6, 6],
+            "sideLengths": [1, 2, 3, 4, 5],
         }
 
     def test_error_if_file_doesnt_look_like_object(self, js_path: pathlib.Path) -> None:
+        """
+        Appending to a file which doesn't contain a JSON object throws
+        a ValueError.
+        """
         shapes = ["apple", "banana", "cherry"]
 
         write_js(js_path, value=shapes, varname="fruit")
@@ -148,8 +263,30 @@ class TestAppendToObject:
         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])
 
+    def test_error_if_path_doesnt_exist(self, js_path: pathlib.Path) -> None:
+        """
+        Appending to the path of a non-existent file throws FileNotFoundError.
+        """
+        with pytest.raises(FileNotFoundError):
+            append_to_js_object(js_path, key="name", value="alex")
+
+    def test_error_if_path_is_dir(self, tmp_path: pathlib.Path) -> None:
+        """
+        Appending to a path which is a directory throws IsADirectoryError.
+        """
+        with pytest.raises(IsADirectoryError):
+            append_to_js_object(tmp_path, key="name", value="alex")
+
 
 class TestRoundTrip:
+    """
+    A "round trip" is a test that we can use one function to store a value,
+    and another function to retrieve it.
+
+    It checks that the functions are consistent with each other, and aren't
+    losing any information along the way.
+    """
+
     @pytest.mark.parametrize(
         "value",
         [
@@ -166,10 +303,18 @@ class TestRoundTrip:
     def test_can_read_and_write_value(
         self, js_path: pathlib.Path, value: typing.Any
     ) -> None:
+        """
+        After you write a value with ``write_js()``, you get the same value
+        back when you call ``read_js()``.
+        """
         write_js(js_path, value=value, varname="myTestVariable")
         assert read_js(js_path, varname="myTestVariable") == value
 
     def test_can_append_to_file(self, js_path: pathlib.Path) -> None:
+        """
+        After you append a value to an array, you can read the entire file
+        and get the updated array.
+        """
         write_js(js_path, value=["apple", "banana", "coconut"], varname="fruit")
         append_to_js_array(js_path, value="damson")
         assert read_js(js_path, varname="fruit") == [