From 1569f9623334fc5dd36066e8f9bac26bd89c7532 Mon Sep 17 00:00:00 2001 From: Leon Zhao Date: Wed, 13 May 2026 10:55:41 +0800 Subject: [PATCH 1/2] chore: upgrade loro py to 1.12 --- Cargo.lock | 18 +++++----- Cargo.toml | 4 +-- loro.pyi | 50 +++++++++++++++++++++++++++ src/doc.rs | 83 ++++++++++++++++++++++++++++++++++++++++----- tests/test_basic.py | 11 ++++++ 5 files changed, 146 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e54d98..464f720 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,9 +510,9 @@ dependencies = [ [[package]] name = "loro" -version = "1.10.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75216d8f99725531a30f7b00901ee154a4f8a9b7f125bfe032e197d4c7ffb8c" +checksum = "adc16ee5fdda7bff6bbbd4ff276c31aa9747bc90ad1bdccf8f0da97cd2949c8a" dependencies = [ "enum-as-inner 0.6.1", "generic-btree", @@ -526,9 +526,9 @@ dependencies = [ [[package]] name = "loro-common" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70363ea05a9c507fd9d58b65dc414bf515f636d69d8ab53e50ecbe8d27eef90c" +checksum = "193e88dedf3bc07f3b25ec8bb609dd461349b26942e43933cb0f599bc09d9c5b" dependencies = [ "arbitrary", "enum-as-inner 0.6.1", @@ -556,9 +556,9 @@ dependencies = [ [[package]] name = "loro-internal" -version = "1.10.3" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f447044ec3d3ba572623859add3334bd87b84340ee5fdf00315bfee0e3ad3e3f" +checksum = "8d42db22ea93c266d5b6ef09ba94af080b6a4d131942e9a28bfa6a218b312b5f" dependencies = [ "append-only-bytes", "arref", @@ -603,9 +603,9 @@ dependencies = [ [[package]] name = "loro-kv-store" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78beebc933a33c26495c9a98f05b38bc0a4c0a337ecfbd3146ce1f9437eec71f" +checksum = "18853eed186c39e0b9d541a1f847161ad05bcf366c412068c9d257d5d981a9b5" dependencies = [ "bytes", "ensure-cov", @@ -648,7 +648,7 @@ dependencies = [ [[package]] name = "loro_py" -version = "1.10.3" +version = "1.12.0" dependencies = [ "loro", "pyo3", diff --git a/Cargo.toml b/Cargo.toml index 11b1dac..62ef10b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "loro_py" -version = "1.10.3" +version = "1.12.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -9,7 +9,7 @@ name = "loro" crate-type = ["cdylib"] [dependencies] -loro = { version = "1.10.3", features = ["counter", "jsonpath"] } +loro = { version = "1.12.0", features = ["counter", "jsonpath"] } rustc-hash = "2.1.1" pyo3 = { version = "0.26.0" } serde_json = "1" diff --git a/loro.pyi b/loro.pyi index 9107b4e..ee369ca 100644 --- a/loro.pyi +++ b/loro.pyi @@ -425,6 +425,16 @@ class LoroDoc: """ ... + def try_get_movable_list( + self, obj: ContainerId + ) -> typing.Optional[LoroMovableList]: + r""" + Try to get a [LoroMovableList] by container id. + + Returns `None` if the container does not exist. + """ + ... + def get_list(self, obj: ContainerId) -> LoroList: r""" Get a [LoroList] by container id. @@ -433,6 +443,14 @@ class LoroDoc: """ ... + def try_get_list(self, obj: ContainerId) -> typing.Optional[LoroList]: + r""" + Try to get a [LoroList] by container id. + + Returns `None` if the container does not exist. + """ + ... + def get_map(self, obj: ContainerId) -> LoroMap: r""" Get a [LoroMap] by container id. @@ -441,6 +459,14 @@ class LoroDoc: """ ... + def try_get_map(self, obj: ContainerId) -> typing.Optional[LoroMap]: + r""" + Try to get a [LoroMap] by container id. + + Returns `None` if the container does not exist. + """ + ... + def get_text(self, obj: ContainerId) -> LoroText: r""" Get a [LoroText] by container id. @@ -449,6 +475,14 @@ class LoroDoc: """ ... + def try_get_text(self, obj: ContainerId) -> typing.Optional[LoroText]: + r""" + Try to get a [LoroText] by container id. + + Returns `None` if the container does not exist. + """ + ... + def get_tree(self, obj: ContainerId) -> LoroTree: r""" Get a [LoroTree] by container id. @@ -457,6 +491,14 @@ class LoroDoc: """ ... + def try_get_tree(self, obj: ContainerId) -> typing.Optional[LoroTree]: + r""" + Try to get a [LoroTree] by container id. + + Returns `None` if the container does not exist. + """ + ... + def get_counter(self, obj: typing.Any) -> LoroCounter: r""" Get a [LoroCounter] by container id. @@ -465,6 +507,14 @@ class LoroDoc: """ ... + def try_get_counter(self, obj: typing.Any) -> typing.Optional[LoroCounter]: + r""" + Try to get a [LoroCounter] by container id. + + Returns `None` if the container does not exist. + """ + ... + def commit(self) -> None: r""" Commit the cumulative auto commit transaction. diff --git a/src/doc.rs b/src/doc.rs index cc1e80e..23cb0e6 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -76,9 +76,9 @@ impl LoroDoc { /// Fork the document at the given frontiers. /// /// The created doc will only contain the history before the specified frontiers. - pub fn fork_at(&self, frontiers: &Frontiers) -> Self { - let new_doc = self.doc.fork_at(&frontiers.into()); - Self { doc: new_doc } + pub fn fork_at(&self, frontiers: &Frontiers) -> PyLoroResult { + let new_doc = self.doc.fork_at(&frontiers.into())?; + Ok(Self { doc: new_doc }) } /// Get the configurations of the document. @@ -287,6 +287,21 @@ impl LoroDoc { Ok(LoroMovableList(self.doc.get_movable_list(container_id))) } + /// Try to get a [LoroMovableList] by container id. + /// + /// Returns `None` if the container does not exist. + #[inline] + pub fn try_get_movable_list( + &self, + obj: &Bound<'_, PyAny>, + ) -> PyResult> { + let container_id = pyobject_to_container_id(obj, ContainerType::MovableList {})?; + Ok(self + .doc + .try_get_movable_list(container_id) + .map(LoroMovableList)) + } + /// Get a [LoroList] by container id. /// /// If the provided id is string, it will be converted into a root container id with the name of the string. @@ -296,6 +311,15 @@ impl LoroDoc { Ok(LoroList(self.doc.get_list(container_id))) } + /// Try to get a [LoroList] by container id. + /// + /// Returns `None` if the container does not exist. + #[inline] + pub fn try_get_list(&self, obj: &Bound<'_, PyAny>) -> PyResult> { + let container_id = pyobject_to_container_id(obj, ContainerType::List {})?; + Ok(self.doc.try_get_list(container_id).map(LoroList)) + } + /// Get a [LoroMap] by container id. /// /// If the provided id is string, it will be converted into a root container id with the name of the string. @@ -305,6 +329,15 @@ impl LoroDoc { Ok(LoroMap(self.doc.get_map(container_id))) } + /// Try to get a [LoroMap] by container id. + /// + /// Returns `None` if the container does not exist. + #[inline] + pub fn try_get_map(&self, obj: &Bound<'_, PyAny>) -> PyResult> { + let container_id = pyobject_to_container_id(obj, ContainerType::Map {})?; + Ok(self.doc.try_get_map(container_id).map(LoroMap)) + } + /// Get a [LoroText] by container id. /// /// If the provided id is string, it will be converted into a root container id with the name of the string. @@ -314,6 +347,15 @@ impl LoroDoc { Ok(LoroText(self.doc.get_text(container_id))) } + /// Try to get a [LoroText] by container id. + /// + /// Returns `None` if the container does not exist. + #[inline] + pub fn try_get_text(&self, obj: &Bound<'_, PyAny>) -> PyResult> { + let container_id = pyobject_to_container_id(obj, ContainerType::Text {})?; + Ok(self.doc.try_get_text(container_id).map(LoroText)) + } + /// Get a [LoroTree] by container id. /// /// If the provided id is string, it will be converted into a root container id with the name of the string. @@ -323,6 +365,15 @@ impl LoroDoc { Ok(LoroTree(self.doc.get_tree(container_id))) } + /// Try to get a [LoroTree] by container id. + /// + /// Returns `None` if the container does not exist. + #[inline] + pub fn try_get_tree(&self, obj: &Bound<'_, PyAny>) -> PyResult> { + let container_id = pyobject_to_container_id(obj, ContainerType::Tree {})?; + Ok(self.doc.try_get_tree(container_id).map(LoroTree)) + } + /// Get a [LoroCounter] by container id. /// /// If the provided id is string, it will be converted into a root container id with the name of the string. @@ -332,6 +383,15 @@ impl LoroDoc { Ok(LoroCounter(self.doc.get_counter(container_id))) } + /// Try to get a [LoroCounter] by container id. + /// + /// Returns `None` if the container does not exist. + #[inline] + pub fn try_get_counter(&self, obj: &Bound<'_, PyAny>) -> PyResult> { + let container_id = pyobject_to_container_id(obj, ContainerType::Counter {})?; + Ok(self.doc.try_get_counter(container_id).map(LoroCounter)) + } + /// Commit the cumulative auto commit transaction. /// /// There is a transaction behind every operation. @@ -872,14 +932,19 @@ impl LoroDoc { /// /// The callback may fire false positives; it is intended as a lightweight notification so /// callers can debounce or throttle before running an expensive JSONPath query themselves. - pub fn subscribe_jsonpath(&self, path: &str, callback: Py) -> PyLoroResult { - let subscription = self - .doc - .subscribe_jsonpath(path, Arc::new(move || { + pub fn subscribe_jsonpath( + &self, + path: &str, + callback: Py, + ) -> PyLoroResult { + let subscription = self.doc.subscribe_jsonpath( + path, + Arc::new(move || { Python::attach(|py| { callback.call0(py).unwrap(); }); - }))?; + }), + )?; Ok(subscription.into()) } @@ -1124,7 +1189,7 @@ impl Configure { } pub fn text_style_config(&self) -> StyleConfigMap { - StyleConfigMap(self.0.text_style_config().read().unwrap().clone()) + StyleConfigMap(self.0.text_style_config().read().clone()) } pub fn record_timestamp(&self) -> bool { diff --git a/tests/test_basic.py b/tests/test_basic.py index bab4880..6f4c179 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -32,3 +32,14 @@ def test_apply_diff(): doc1.commit() doc2.apply_diff(doc1.diff(f1, doc1.oplog_frontiers)) assert doc2.get_text("text").to_string() == "abc" + +def test_try_get_container_methods(): + doc = LoroDoc() + text = doc.get_text("text") + text.insert(0, "abc") + doc.commit() + + assert doc.try_get_text("text").to_string() == "abc" + assert doc.try_get_text("missing") is not None + assert doc.try_get_text(text.id) is not None + assert doc.try_get_map(text.id) is None From 4ebbbabcd7be08952b7c098e75789a3696a30240 Mon Sep 17 00:00:00 2001 From: Leon Zhao Date: Sun, 14 Jun 2026 15:11:06 +0800 Subject: [PATCH 2/2] chore: upgrade loro py to 1.13.1 --- Cargo.lock | 26 +++++++++++++------------- Cargo.toml | 4 ++-- loro.pyi | 12 ++++++++++++ src/container/map.rs | 26 +++++++++++++++++++++++++- tests/test_map.py | 33 ++++++++++++++++++++++++++++++++- 5 files changed, 84 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 464f720..ade2835 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,9 +510,9 @@ dependencies = [ [[package]] name = "loro" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc16ee5fdda7bff6bbbd4ff276c31aa9747bc90ad1bdccf8f0da97cd2949c8a" +checksum = "7bd1b63cd2f0cf66acbbb3191c41feaff03c94c3f6c6bb1e61d2ab3062c301c7" dependencies = [ "enum-as-inner 0.6.1", "generic-btree", @@ -526,9 +526,9 @@ dependencies = [ [[package]] name = "loro-common" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193e88dedf3bc07f3b25ec8bb609dd461349b26942e43933cb0f599bc09d9c5b" +checksum = "b51042936156a1d537df8a38938ec8ed24b45de14530af933c1d02de39c9e056" dependencies = [ "arbitrary", "enum-as-inner 0.6.1", @@ -544,9 +544,9 @@ dependencies = [ [[package]] name = "loro-delta" -version = "1.9.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eafa788a72c1cbf0b7dc08a862cd7cc31b96d99c2ef749cdc94c2330f9494d3" +checksum = "96f381e7fb5d4bf5605b4f4a9241f3adf61403ded978d757552004c8302cccbb" dependencies = [ "arrayvec", "enum-as-inner 0.5.1", @@ -556,9 +556,9 @@ dependencies = [ [[package]] name = "loro-internal" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d42db22ea93c266d5b6ef09ba94af080b6a4d131942e9a28bfa6a218b312b5f" +checksum = "252a84196402ac55a7dd064af87a747a430fc176ec744d5bb99f4ed92e639fa8" dependencies = [ "append-only-bytes", "arref", @@ -603,9 +603,9 @@ dependencies = [ [[package]] name = "loro-kv-store" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18853eed186c39e0b9d541a1f847161ad05bcf366c412068c9d257d5d981a9b5" +checksum = "950cc8bcc64dcff536e949fbc56ccc3d0759646fa441c7f76b3cd7c3eafa2096" dependencies = [ "bytes", "ensure-cov", @@ -637,9 +637,9 @@ checksum = "3f3d053a135388e6b1df14e8af1212af5064746e9b87a06a345a7a779ee9695a" [[package]] name = "loro_fractional_index" -version = "1.6.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c8ea186958094052b971fe7e322a934b034c3bf62f0458ccea04fcd687ba1" +checksum = "aca7180674d0273ddf37049a5efcde4547fd5330d24abb7519bb9d9eb6780d5b" dependencies = [ "once_cell", "rand", @@ -648,7 +648,7 @@ dependencies = [ [[package]] name = "loro_py" -version = "1.12.0" +version = "1.13.1" dependencies = [ "loro", "pyo3", diff --git a/Cargo.toml b/Cargo.toml index 62ef10b..ae7c41c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "loro_py" -version = "1.12.0" +version = "1.13.1" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -9,7 +9,7 @@ name = "loro" crate-type = ["cdylib"] [dependencies] -loro = { version = "1.12.0", features = ["counter", "jsonpath"] } +loro = { version = "1.13.1", features = ["counter", "jsonpath"] } rustc-hash = "2.1.1" pyo3 = { version = "0.26.0" } serde_json = "1" diff --git a/loro.pyi b/loro.pyi index ee369ca..dea128b 100644 --- a/loro.pyi +++ b/loro.pyi @@ -1356,6 +1356,18 @@ class LoroMap: """ ... + def ensure_mergeable_list(self, key: str) -> LoroList: ... + + def ensure_mergeable_map(self, key: str) -> LoroMap: ... + + def ensure_mergeable_tree(self, key: str) -> LoroTree: ... + + def ensure_mergeable_movable_list(self, key: str) -> LoroMovableList: ... + + def ensure_mergeable_text(self, key: str) -> LoroText: ... + + def ensure_mergeable_counter(self, key: str) -> LoroCounter: ... + def clear(self) -> None: r""" Delete all key-value pairs in the map. diff --git a/src/container/map.rs b/src/container/map.rs index ec2c3ac..b8390dc 100644 --- a/src/container/map.rs +++ b/src/container/map.rs @@ -10,7 +10,7 @@ use crate::{ value::{ContainerID, LoroValue, ValueOrContainer}, }; -use super::Container; +use super::{Container, LoroCounter, LoroList, LoroMovableList, LoroText, LoroTree}; pub fn register_class(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; @@ -150,6 +150,30 @@ impl LoroMap { Ok(container.into()) } + pub fn ensure_mergeable_list(&self, key: &str) -> PyLoroResult { + Ok(LoroList(self.0.ensure_mergeable_list(key)?)) + } + + pub fn ensure_mergeable_map(&self, key: &str) -> PyLoroResult { + Ok(LoroMap(self.0.ensure_mergeable_map(key)?)) + } + + pub fn ensure_mergeable_tree(&self, key: &str) -> PyLoroResult { + Ok(LoroTree(self.0.ensure_mergeable_tree(key)?)) + } + + pub fn ensure_mergeable_movable_list(&self, key: &str) -> PyLoroResult { + Ok(LoroMovableList(self.0.ensure_mergeable_movable_list(key)?)) + } + + pub fn ensure_mergeable_text(&self, key: &str) -> PyLoroResult { + Ok(LoroText(self.0.ensure_mergeable_text(key)?)) + } + + pub fn ensure_mergeable_counter(&self, key: &str) -> PyLoroResult { + Ok(LoroCounter(self.0.ensure_mergeable_counter(key)?)) + } + /// Delete all key-value pairs in the map. pub fn clear(&self) -> PyLoroResult<()> { self.0.clear()?; diff --git a/tests/test_map.py b/tests/test_map.py index 18048bd..067421c 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -1,4 +1,6 @@ -from loro import LoroDoc, LoroList +import pytest + +from loro import ExportMode, LoroDoc, LoroList def test_map(): doc = LoroDoc() @@ -12,3 +14,32 @@ def test_map(): } map["key2"] = "value2" assert map["key2"].value == "value2" + +def test_ensure_mergeable_map_converges(): + doc1 = LoroDoc() + doc1.peer_id = 1 + doc2 = LoroDoc() + doc2.peer_id = 2 + + profile1 = doc1.get_map("root").ensure_mergeable_map("profile") + profile1.insert("name", "Ada") + doc1.commit() + + profile2 = doc2.get_map("root").ensure_mergeable_map("profile") + profile2.insert("age", 37) + doc2.commit() + + doc1.import_(doc2.export(ExportMode.Snapshot())) + doc2.import_(doc1.export(ExportMode.Snapshot())) + + expected = {"root": {"profile": {"name": "Ada", "age": 37}}} + assert doc1.get_deep_value() == expected + assert doc2.get_deep_value() == expected + +def test_ensure_mergeable_rejects_occupied_key(): + doc = LoroDoc() + root = doc.get_map("root") + root.insert("profile", "occupied") + + with pytest.raises(BaseException): + root.ensure_mergeable_map("profile")