Skip to content

Commit 4481cf4

Browse files
Add 100% coverage (#73)
1 parent b7cc5ae commit 4481cf4

File tree

13 files changed

+286
-32
lines changed

13 files changed

+286
-32
lines changed

.github/workflows/test.yml

+11-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ on:
88
branches:
99
- main
1010

11+
defaults:
12+
run:
13+
shell: bash
14+
1115
jobs:
1216
test:
1317
name: ${{ matrix.os }} python-${{ matrix.python-version }}
@@ -34,10 +38,16 @@ jobs:
3438
create-args: python=${{ matrix.python-version }}
3539

3640
- name: Build and install pycrdt
37-
run: pip install .[test]
41+
run: pip install -e ".[test]"
3842

3943
- name: Check types
4044
run: mypy python
4145

4246
- name: Run tests
4347
run: pytest --color=yes -v tests
48+
49+
- name: Run code coverage
50+
if: ${{ (matrix.python-version == '3.12') && (matrix.os == 'ubuntu-latest') }}
51+
run: |
52+
coverage run -m pytest tests
53+
coverage report --show-missing --fail-under=100

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[![Build Status](https://github.com/jupyter-server/pycrdt/actions/workflows/test.yml/badge.svg?query=branch%3Amain++)](https://github.com/jupyter-server/pycrdt/actions/workflows/test.yml/badge.svg?query=branch%3Amain++)
2+
[![Code Coverage](https://img.shields.io/badge/coverage-100%25-green)](https://img.shields.io/badge/coverage-100%25-green)
23
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
34

45
⚠️ This project is still in an **incubating** phase (i.e. it's not ready for production yet) ⚠️

pyproject.toml

+7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ test = [
3535
"y-py >=0.7.0a1,<0.8",
3636
"pydantic >=2.5.2,<3",
3737
"mypy",
38+
"coverage[toml] >=7",
3839
]
3940
docs = [ "mkdocs", "mkdocs-material" ]
4041

@@ -49,3 +50,9 @@ module-name = "pycrdt._pycrdt"
4950
[tool.ruff]
5051
line-length = 100
5152
select = ["F", "E", "W", "I001"]
53+
54+
[tool.coverage.run]
55+
source = ["python", "tests"]
56+
57+
[tool.coverage.report]
58+
show_missing = true

python/pycrdt/array.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ._pycrdt import ArrayEvent as _ArrayEvent
88
from .base import BaseDoc, BaseEvent, BaseType, base_types, event_types
99

10-
if TYPE_CHECKING:
10+
if TYPE_CHECKING: # pragma: no cover
1111
from .doc import Doc
1212

1313

@@ -106,16 +106,16 @@ def __setitem__(self, key: int | slice, value: Any | list[Any]) -> None:
106106
raise RuntimeError("Step not supported")
107107
if key.start != key.stop:
108108
raise RuntimeError("Start and stop must be equal")
109-
if len(self) <= key.start < 0:
109+
if key.start > len(self) or key.start < 0:
110110
raise RuntimeError("Index out of range")
111111
for i, v in enumerate(value):
112112
self._set(i + key.start, v)
113113
else:
114-
raise RuntimeError(f"Index not supported: {key}")
114+
raise RuntimeError("Index must be of type integer")
115115

116116
def _check_index(self, idx: int) -> int:
117117
if not isinstance(idx, int):
118-
raise RuntimeError("Index must be of type int")
118+
raise RuntimeError("Index must be of type integer")
119119
length = len(self)
120120
if idx < 0:
121121
idx += length
@@ -146,7 +146,9 @@ def __delitem__(self, key: int | slice) -> None:
146146
n = key.stop - i
147147
self.integrated.remove_range(txn._txn, i, n)
148148
else:
149-
raise RuntimeError(f"Index not supported: {key}")
149+
raise TypeError(
150+
f"array indices must be integers or slices, not {type(key).__name__}"
151+
)
150152

151153
def __getitem__(self, key: int) -> BaseType:
152154
with self.doc.transaction() as txn:
@@ -163,7 +165,7 @@ def __iter__(self):
163165
return ArrayIterator(self)
164166

165167
def __contains__(self, item: Any) -> bool:
166-
return item in iter(self)
168+
return item in list(self)
167169

168170
def __str__(self) -> str:
169171
with self.doc.transaction() as txn:

python/pycrdt/base.py

+1-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ._pycrdt import Transaction as _Transaction
88
from .transaction import ReadTransaction, Transaction
99

10-
if TYPE_CHECKING:
10+
if TYPE_CHECKING: # pragma: no cover
1111
from .doc import Doc
1212

1313

@@ -77,14 +77,6 @@ def _forbid_read_transaction(self, txn: Transaction):
7777
"Read-only transaction cannot be used to modify document structure"
7878
)
7979

80-
def _current_transaction(self) -> Transaction:
81-
if self._doc is None:
82-
raise RuntimeError("Not associated with a document")
83-
if self._doc._txn is None:
84-
raise RuntimeError("No current transaction")
85-
res = cast(Transaction, self._doc._txn)
86-
return res
87-
8880
def _integrate(self, doc: Doc, integrated: Any) -> Any:
8981
prelim = self._prelim
9082
self._doc = doc
@@ -95,8 +87,6 @@ def _integrate(self, doc: Doc, integrated: Any) -> Any:
9587
def _do_and_integrate(
9688
self, action: str, value: BaseType, txn: _Transaction, *args
9789
) -> None:
98-
if value.is_integrated:
99-
raise RuntimeError("Already integrated")
10090
method = getattr(self._integrated, f"{action}_{value.type_name}_prelim")
10191
integrated = method(txn, *args)
10292
assert self._doc is not None

python/pycrdt/doc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def __getitem__(self, key: str) -> BaseType:
7171
return self._roots[key]
7272

7373
def __iter__(self):
74-
return self.keys()
74+
return iter(self.keys())
7575

7676
def keys(self):
7777
return self._roots.keys()

python/pycrdt/map.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ._pycrdt import MapEvent as _MapEvent
88
from .base import BaseDoc, BaseEvent, BaseType, base_types, event_types
99

10-
if TYPE_CHECKING:
10+
if TYPE_CHECKING: # pragma: no cover
1111
from .doc import Doc
1212

1313

@@ -116,7 +116,7 @@ def _check_key(self, key: str):
116116
if not isinstance(key, str):
117117
raise RuntimeError("Key must be of type string")
118118
if key not in self.keys():
119-
raise KeyError(f"KeyError: {key}")
119+
raise KeyError(key)
120120

121121
def keys(self):
122122
with self.doc.transaction() as txn:

python/pycrdt/text.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from ._pycrdt import TextEvent as _TextEvent
88
from .base import BaseEvent, BaseType, base_types, event_types
99

10-
if TYPE_CHECKING:
10+
if TYPE_CHECKING: # pragma: no cover
1111
from .doc import Doc
1212

1313

@@ -92,6 +92,10 @@ def __delitem__(self, key: int | slice) -> None:
9292
else:
9393
raise RuntimeError(f"Index not supported: {key}")
9494

95+
def __getitem__(self, key: int | slice) -> str:
96+
value = str(self)
97+
return value[key]
98+
9599
def __setitem__(self, key: int | slice, value: str) -> None:
96100
with self.doc.transaction() as txn:
97101
self._forbid_read_transaction(txn)

python/pycrdt/transaction.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
from ._pycrdt import Transaction as _Transaction
77

8-
if TYPE_CHECKING:
8+
if TYPE_CHECKING: # pragma: no cover
99
from .doc import Doc
1010

1111

tests/test_array.py

+76-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
from functools import partial
33

4+
import pytest
45
from pycrdt import Array, Doc, Map, Text
56

67

@@ -47,7 +48,7 @@ def test_array():
4748
doc["array"] = array
4849
events = []
4950

50-
array.observe(partial(callback, events))
51+
idx = array.observe(partial(callback, events))
5152
ref = [
5253
-1,
5354
-2,
@@ -62,6 +63,7 @@ def test_array():
6263
-3,
6364
-4,
6465
-6,
66+
-7,
6567
]
6668
with doc.transaction():
6769
array.append("foo")
@@ -79,6 +81,7 @@ def test_array():
7981
array = array + [-3, -4]
8082
array += [-5]
8183
array[-1] = -6
84+
array.extend([-7])
8285

8386
assert json.loads(str(array)) == ref
8487
assert len(array) == len(ref)
@@ -92,24 +95,45 @@ def test_array():
9295
}
9396
]
9497

98+
array.clear()
99+
assert array.to_py() == []
100+
101+
events.clear()
102+
array.unobserve(idx)
103+
array.append("foo")
104+
assert events == []
105+
95106

96107
def test_observe():
97108
doc = Doc()
98109
array = Array()
99110
doc["array"] = array
100111

101-
def callback(e):
102-
pass
103-
104-
sid0 = array.observe(callback)
105-
sid1 = array.observe(callback)
106-
sid2 = array.observe_deep(callback)
107-
sid3 = array.observe_deep(callback)
112+
sid0 = array.observe(lambda x: x)
113+
sid1 = array.observe(lambda x: x)
114+
sid2 = array.observe_deep(lambda x: x)
115+
sid3 = array.observe_deep(lambda x: x)
108116
assert sid0 == "o_0"
109117
assert sid1 == "o_1"
110118
assert sid2 == "od0"
111119
assert sid3 == "od1"
112120

121+
deep_events = []
122+
123+
def cb(events):
124+
deep_events.append(events)
125+
126+
sid4 = array.observe_deep(cb)
127+
array.append("bar")
128+
assert (
129+
str(deep_events[0][0])
130+
== """{target: ["bar"], delta: [{'insert': ['bar']}], path: []}"""
131+
)
132+
deep_events.clear()
133+
array.unobserve(sid4)
134+
array.append("baz")
135+
assert deep_events == []
136+
113137

114138
def test_api():
115139
# pop
@@ -129,6 +153,50 @@ def test_api():
129153
array.insert(1, 4)
130154
assert str(array) == "[1.0,4.0,2.0,3.0]"
131155

156+
# slices
157+
doc = Doc()
158+
array = Array([i for i in range(10)])
159+
doc["array"] = array
160+
with pytest.raises(RuntimeError) as excinfo:
161+
array[::2] = 1
162+
assert str(excinfo.value) == "Step not supported"
163+
with pytest.raises(RuntimeError) as excinfo:
164+
array[1:2] = 1
165+
assert str(excinfo.value) == "Start and stop must be equal"
166+
with pytest.raises(RuntimeError) as excinfo:
167+
array[-1:-1] = 1
168+
assert str(excinfo.value) == "Index out of range"
169+
with pytest.raises(RuntimeError) as excinfo:
170+
array["a"] = 1
171+
assert str(excinfo.value) == "Index must be of type integer"
172+
with pytest.raises(RuntimeError) as excinfo:
173+
array.pop("a")
174+
assert str(excinfo.value) == "Index must be of type integer"
175+
with pytest.raises(IndexError) as excinfo:
176+
array.pop(len(array))
177+
assert str(excinfo.value) == "Array index out of range"
178+
with pytest.raises(RuntimeError) as excinfo:
179+
del array[::2]
180+
assert str(excinfo.value) == "Step not supported"
181+
with pytest.raises(RuntimeError) as excinfo:
182+
del array[-1:]
183+
assert str(excinfo.value) == "Negative start not supported"
184+
with pytest.raises(RuntimeError) as excinfo:
185+
del array[:-1]
186+
assert str(excinfo.value) == "Negative stop not supported"
187+
with pytest.raises(TypeError) as excinfo:
188+
del array["a"]
189+
assert str(excinfo.value) == "array indices must be integers or slices, not str"
190+
191+
assert [value for value in array] == [value for value in range(10)]
192+
assert 1 in array
193+
194+
array = Array([0, 1, 2])
195+
assert array.to_py() == [0, 1, 2]
196+
197+
array = Array()
198+
assert array.to_py() is None
199+
132200

133201
def test_move():
134202
doc = Doc()

tests/test_doc.py

+29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from functools import partial
22

3+
import pytest
34
from pycrdt import Array, Doc, Map, Text
45

56

@@ -18,6 +19,25 @@ def encode_client_id(client_id_bytes):
1819
return bytes(b)
1920

2021

22+
def test_api():
23+
doc = Doc()
24+
25+
with pytest.raises(RuntimeError) as excinfo:
26+
doc[0] = Array()
27+
assert str(excinfo.value) == "Key must be of type string"
28+
29+
doc["a0"] = a0 = Array()
30+
doc["m0"] = m0 = Map()
31+
doc["t0"] = t0 = Text()
32+
assert set((key for key in doc)) == set(("a0", "m0", "t0"))
33+
assert set([type(value) for value in doc.values()]) == set(
34+
[type(value) for value in (a0, m0, t0)]
35+
)
36+
assert set([(key, type(value)) for key, value in doc.items()]) == set(
37+
[(key, type(value)) for key, value in (("a0", a0), ("m0", m0), ("t0", t0))]
38+
)
39+
40+
2141
def test_subdoc():
2242
doc0 = Doc()
2343
map0 = Map()
@@ -77,6 +97,15 @@ def test_subdoc():
7797
assert event.loaded == []
7898

7999

100+
def test_doc_in_event():
101+
doc = Doc()
102+
doc["array"] = array = Array()
103+
events = []
104+
array.observe(partial(callback, events))
105+
array.append(Doc())
106+
assert isinstance(events[0].delta[0]["insert"][0], Doc)
107+
108+
80109
def test_transaction_event():
81110
doc = Doc()
82111
events = []

0 commit comments

Comments
 (0)