Skip to content

Commit 29a25d2

Browse files
Add XML related tests and fixes for found bugs
1 parent 4ae7e94 commit 29a25d2

File tree

5 files changed

+229
-35
lines changed

5 files changed

+229
-35
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ Cargo.lock
55
.coverage
66
/site
77
/dist
8+
_pycrdt.*.pyd

python/pycrdt/_xml.py

+34-32
Original file line numberDiff line numberDiff line change
@@ -135,41 +135,41 @@ def __init__(
135135
_doc: Doc | None = None,
136136
_integrated: _XmlElement | None = None,
137137
) -> None:
138-
if tag is None and attributes is None and contents is None:
139-
init = None
140-
elif (attributes is not None or contents is not None) and tag is None:
141-
raise ValueError("Tag is required if specifying attributes or contents")
138+
"""
139+
Creates a new preliminary element.
140+
141+
`tag` is required.
142+
"""
143+
if _integrated is not None:
144+
super().__init__(init=None, _doc=_doc, _integrated=_integrated)
145+
return
146+
147+
if tag is None:
148+
raise ValueError("XmlElement: tag is required")
149+
150+
if isinstance(attributes, dict):
151+
init_attrs = list(attributes.items())
152+
elif attributes is not None:
153+
init_attrs = list(attributes)
142154
else:
143-
if isinstance(attributes, dict):
144-
init_attrs = list(attributes.items())
145-
elif attributes is not None:
146-
init_attrs = list(attributes)
147-
else:
148-
init_attrs = []
155+
init_attrs = []
149156

150-
init = (
157+
super().__init__(
158+
init=(
151159
tag,
152160
init_attrs,
153161
list(contents) if contents is not None else [],
154162
)
155-
156-
super().__init__(
157-
init=init,
158-
_doc=_doc,
159-
_integrated=_integrated,
160163
)
161164

162165
def to_py(self) -> None:
163166
raise ValueError("XmlElement has no Python equivalent")
164167

165-
def _get_or_insert(self, _name: str, _doc: Doc) -> Any:
168+
def _get_or_insert(self, name: str, doc: Doc) -> Any:
166169
raise ValueError("Cannot get an XmlElement from a doc - get an XmlFragment instead.")
167170

168-
def _init(
169-
self, value: tuple[str, list[tuple[str, str]], list[str | XmlElement | XmlText]] | None
170-
):
171-
if value is None:
172-
return
171+
def _init(self, value: tuple[str, list[tuple[str, str]], list[str | XmlElement | XmlText]] | None):
172+
assert value is not None
173173
_, attrs, contents = value
174174
with self.doc.transaction():
175175
for k, v in attrs:
@@ -179,6 +179,9 @@ def _init(
179179

180180
@property
181181
def tag(self) -> str | None:
182+
"""
183+
Gets the element's tag.
184+
"""
182185
return self.integrated.tag()
183186

184187

@@ -190,12 +193,12 @@ class XmlText(_XmlTraitMixin):
190193
of an `XmlElement` or `XmlFragment`.
191194
"""
192195

193-
_prelim: str | None
196+
_prelim: str
194197
_integrated: _XmlText | None
195198

196199
def __init__(
197200
self,
198-
init: str | None = None,
201+
init: str = "",
199202
*,
200203
_doc: Doc | None = None,
201204
_integrated: _XmlText | None = None,
@@ -209,14 +212,13 @@ def __init__(
209212
def _get_or_insert(self, _name: str, _doc: Doc) -> Any:
210213
raise ValueError("Cannot get an XmlText from a doc - get an XmlFragment instead.")
211214

212-
def to_py(self) -> str | None:
215+
def to_py(self) -> str:
213216
if self._integrated is None:
214217
return self._prelim
215218
return str(self)
216219

217-
def _init(self, value: str | None) -> None:
218-
if value is None:
219-
return
220+
def _init(self, value: str | None) -> None: # pragma: no cover
221+
assert value is not None
220222
with self.doc.transaction() as txn:
221223
self.integrated.insert(txn._txn, 0, value)
222224

@@ -236,7 +238,7 @@ def insert(self, index: int, value: str, attrs: Mapping[str, Any] | None = None)
236238
with self.doc.transaction() as txn:
237239
self._forbid_read_transaction(txn)
238240
self.integrated.insert(
239-
txn._txn, index, value, attrs.items() if attrs is not None else iter([])
241+
txn._txn, index, value, iter(attrs.items()) if attrs is not None else iter([])
240242
)
241243

242244
def insert_embed(self, index: int, value: Any, attrs: dict[str, Any] | None = None) -> None:
@@ -283,7 +285,7 @@ def __delitem__(self, key: int | slice) -> None:
283285
if length > 0:
284286
self.integrated.remove_range(txn._txn, start, length)
285287
else:
286-
raise RuntimeError(f"Index not supported: {key}")
288+
raise TypeError(f"Index not supported: {key}")
287289

288290
def clear(self) -> None:
289291
"""Remove the entire range of characters."""
@@ -407,7 +409,7 @@ def __delitem__(self, key: int | slice) -> None:
407409
if length > 0:
408410
self.inner.integrated.remove_range(txn._txn, start, length)
409411
else:
410-
raise RuntimeError(f"Index not supported: {key}")
412+
raise TypeError(f"Index not supported: {key}")
411413

412414
def __setitem__(self, key: int, value: str | XmlText | XmlElement):
413415
"""
@@ -459,7 +461,7 @@ def insert(self, index: int, element: str | XmlText | XmlElement) -> XmlText | X
459461
element._init(prelim)
460462
return element
461463
else:
462-
raise ValueError("Cannot add value to XML: " + repr(element))
464+
raise TypeError("Cannot add value to XML: " + repr(element))
463465

464466
@overload
465467
def append(self, element: str | XmlText) -> XmlText: ...

src/text.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ impl Text {
5151

5252
#[pyo3(signature = (txn, index, embed, attrs=None))]
5353
fn insert_embed(&self, txn: &mut Transaction, index: u32, embed: Bound<'_, PyAny>, attrs: Option<Bound<'_, PyIterator>>) -> PyResult<()> {
54+
let embed = py_to_any(&embed);
5455
let mut _t = txn.transaction();
5556
let mut t = _t.as_mut().unwrap().as_mut();
56-
let embed = py_to_any(&embed);
5757
if let Some(attrs) = attrs {
5858
let attrs = py_to_attrs(attrs)?;
5959
self.text.insert_embed_with_attributes(&mut t, index, embed, attrs);

src/xml.rs

+16-2
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11

22
use pyo3::types::{PyAnyMethods, PyDict, PyIterator, PyList, PyString, PyTuple};
3-
use pyo3::{pyclass, pymethods, Bound, IntoPy as _, PyObject, PyResult, Python};
3+
use pyo3::{pyclass, pymethods, Bound, IntoPy as _, PyAny, PyObject, PyResult, Python};
44
use yrs::types::text::YChange;
55
use yrs::types::xml::{XmlEvent as _XmlEvent, XmlTextEvent as _XmlTextEvent};
66
use yrs::{
77
DeepObservable, GetString as _, Observable as _, Text as _, TransactionMut, Xml as _, XmlElementPrelim, XmlElementRef, XmlFragment as _, XmlFragmentRef, XmlOut, XmlTextPrelim, XmlTextRef
88
};
99

1010
use crate::subscription::Subscription;
11-
use crate::type_conversions::{events_into_py, py_to_attrs, EntryChangeWrapper};
11+
use crate::type_conversions::{events_into_py, py_to_any, py_to_attrs, EntryChangeWrapper};
1212
use crate::{transaction::Transaction, type_conversions::ToPython};
1313

1414
/// Implements methods common to `XmlFragment`, `XmlElement`, and `XmlText`.
@@ -221,6 +221,20 @@ impl_xml_methods!(XmlText[text, xml: text] {
221221
Ok(())
222222
}
223223

224+
#[pyo3(signature = (txn, index, embed, attrs=None))]
225+
fn insert_embed<'py>(&self, txn: &mut Transaction, index: u32, embed: Bound<'py, PyAny>, attrs: Option<Bound<'_, PyIterator>>) -> PyResult<()> {
226+
let embed = py_to_any(&embed);
227+
let mut _t = txn.transaction();
228+
let mut t = _t.as_mut().unwrap().as_mut();
229+
if let Some(attrs) = attrs {
230+
let attrs = py_to_attrs(attrs)?;
231+
self.text.insert_embed_with_attributes(&mut t, index, embed, attrs);
232+
} else {
233+
self.text.insert_embed(&mut t, index, embed);
234+
}
235+
Ok(())
236+
}
237+
224238
fn remove_range(&self, txn: &mut Transaction, index: u32, len: u32) {
225239
let mut _t = txn.transaction();
226240
let mut t = _t.as_mut().unwrap().as_mut();

tests/test_xml.py

+177
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,20 @@ def test_api():
3333
frag.doc
3434
assert str(excinfo.value) == "Not integrated in a document yet"
3535

36+
with pytest.raises(ValueError):
37+
frag.to_py()
38+
3639
doc["test"] = frag
40+
assert frag.parent is None
3741
assert str(frag) == 'Hello <em class="bold">World</em>!'
3842
assert len(frag.children) == 3
3943
assert str(frag.children[0]) == "Hello "
4044
assert str(frag.children[1]) == '<em class="bold">World</em>'
4145
assert str(frag.children[2]) == "!"
4246
assert list(frag.children) == [frag.children[0], frag.children[1], frag.children[2]]
47+
assert frag.children[0].parent == frag
48+
assert hash(frag.children[0].parent) == hash(frag)
49+
assert frag != object()
4350

4451
frag.children.insert(1, XmlElement("strong", None, ["wonderful"]))
4552
frag.children.insert(2, " ")
@@ -63,6 +70,176 @@ def test_api():
6370
assert str(frag) == 'Hello <em class="bold">World</em>!'
6471

6572

73+
def test_text():
74+
text = XmlText("Hello")
75+
assert text.to_py() == "Hello"
76+
77+
doc = Doc()
78+
79+
with pytest.raises(ValueError):
80+
doc["test"] = XmlText("test")
81+
82+
doc["test"] = XmlFragment([text])
83+
84+
assert str(text) == "Hello"
85+
assert text.to_py() == "Hello"
86+
assert len(text) == len("Hello")
87+
88+
text.clear()
89+
assert str(text) == ""
90+
91+
text += "Goodbye"
92+
assert str(text) == "Goodbye"
93+
94+
text.insert(1, " ")
95+
assert str(text) == "G oodbye"
96+
del text[1]
97+
assert str(text) == "Goodbye"
98+
99+
text.insert(1, " ")
100+
del text[1:3]
101+
assert str(text) == "Goodbye"
102+
103+
assert text.diff() == [("Goodbye", None)]
104+
text.format(1, 3, {"bold": True})
105+
assert text.diff() == [
106+
("G", None),
107+
("oo", {"bold": True}),
108+
("dbye", None),
109+
]
110+
111+
text.insert_embed(0, b"PNG!", {"type": "image"})
112+
assert text.diff() == [
113+
(b"PNG!", {"type": "image"}),
114+
("G", None),
115+
("oo", {"bold": True}),
116+
("dbye", None),
117+
]
118+
119+
text.insert(len(text), " World!", {"href": "some-url"})
120+
assert text.diff() == [
121+
(b"PNG!", {"type": "image"}),
122+
("G", None),
123+
("oo", {"bold": True}),
124+
("dbye", None),
125+
(" World!", {"href": "some-url"}),
126+
]
127+
128+
del text[0]
129+
assert text.diff() == [
130+
("G", None),
131+
("oo", {"bold": True}),
132+
("dbye", None),
133+
(" World!", {"href": "some-url"}),
134+
]
135+
136+
del text[0:3]
137+
assert text.diff() == [
138+
("dbye", None),
139+
(" World!", {"href": "some-url"}),
140+
]
141+
142+
with pytest.raises(RuntimeError):
143+
del text[0:5:2]
144+
with pytest.raises(RuntimeError):
145+
del text[-1:5]
146+
with pytest.raises(RuntimeError):
147+
del text[1:-1]
148+
with pytest.raises(TypeError):
149+
del text["invalid"]
150+
151+
doc["test2"] = XmlFragment([XmlText()])
152+
153+
154+
def test_element():
155+
doc = Doc()
156+
157+
with pytest.raises(ValueError):
158+
doc["test"] = XmlElement("test")
159+
160+
with pytest.raises(ValueError):
161+
XmlElement()
162+
163+
doc["test"] = frag = XmlFragment()
164+
165+
el = XmlElement("div", {"class": "test"})
166+
frag.children.append(el)
167+
assert str(el) == '<div class="test"></div>'
168+
169+
el = XmlElement("div", [("class", "test")])
170+
frag.children.append(el)
171+
assert str(el) == '<div class="test"></div>'
172+
173+
el = XmlElement("div", None, [XmlText("Test")])
174+
frag.children.append(el)
175+
assert str(el) == "<div>Test</div>"
176+
177+
el = XmlElement("div")
178+
frag.children.append(el)
179+
assert str(el) == "<div></div>"
180+
181+
with pytest.raises(ValueError):
182+
el.to_py()
183+
184+
el.attributes["class"] = "test"
185+
assert str(el) == '<div class="test"></div>'
186+
assert "class" in el.attributes
187+
assert el.attributes["class"] == "test"
188+
assert el.attributes.get("class") == "test"
189+
assert len(el.attributes) == 1
190+
assert list(el.attributes) == [("class", "test")]
191+
192+
del el.attributes["class"]
193+
assert str(el) == "<div></div>"
194+
assert "class" not in el.attributes
195+
assert el.attributes.get("class") is None
196+
assert len(el.attributes) == 0
197+
assert list(el.attributes) == []
198+
199+
node = XmlText("Hello")
200+
el.children.append(node)
201+
assert str(el) == "<div>Hello</div>"
202+
assert len(el.children) == 1
203+
assert str(el.children[0]) == "Hello"
204+
assert list(el.children) == [node]
205+
206+
el.children[0] = XmlText("Goodbye")
207+
assert str(el) == "<div>Goodbye</div>"
208+
209+
del el.children[0]
210+
assert str(el) == "<div></div>"
211+
212+
el.children.append(XmlElement("foo"))
213+
el.children.append(XmlElement("bar"))
214+
el.children.append(XmlElement("baz"))
215+
assert str(el) == "<div><foo></foo><bar></bar><baz></baz></div>"
216+
217+
del el.children[0:2]
218+
assert str(el) == "<div><baz></baz></div>"
219+
220+
with pytest.raises(TypeError):
221+
del el.children["invalid"]
222+
with pytest.raises(IndexError):
223+
el.children[1]
224+
225+
text = XmlText("foo")
226+
el.children.insert(0, text)
227+
assert str(el) == "<div>foo<baz></baz></div>"
228+
229+
el2 = XmlElement("bar")
230+
el.children.insert(1, el2)
231+
assert str(el) == "<div>foo<bar></bar><baz></baz></div>"
232+
233+
with pytest.raises(IndexError):
234+
el.children.insert(10, "test")
235+
with pytest.raises(ValueError):
236+
el.children.append(text)
237+
with pytest.raises(ValueError):
238+
el.children.append(el2)
239+
with pytest.raises(TypeError):
240+
el.children.append(object())
241+
242+
66243
def test_observe():
67244
doc = Doc()
68245
doc["test"] = fragment = XmlFragment(["Hello world!"])

0 commit comments

Comments
 (0)