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") == [