Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pycrdt for openSUSE:Factory checked in at 2026-03-23 17:12:19 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pycrdt (Old) and /work/SRC/openSUSE:Factory/.python-pycrdt.new.8177 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pycrdt" Mon Mar 23 17:12:19 2026 rev:15 rq:1341871 version:0.12.50 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pycrdt/python-pycrdt.changes 2025-12-18 18:31:18.341244760 +0100 +++ /work/SRC/openSUSE:Factory/.python-pycrdt.new.8177/python-pycrdt.changes 2026-03-23 17:12:58.280686964 +0100 @@ -1,0 +2,17 @@ +Sun Mar 22 16:41:48 UTC 2026 - Ben Greiner <[email protected]> + +- Update to 0.12.50 + * Fix concurrent async transactions. +- Release 0.12.49 + * Remove use of cache for observer callbacks. +- Release 0.12.48 + * Fix tuple handling (convert to list). +- Release 0.12.47 + * Build wheels for `musllinux` platform. +- Release 0.12.46 + * Bump `pyo3` to v0.28.0. +- Release 0.12.45 + * Raise all exceptions from observer callbacks in an exception + group. + +------------------------------------------------------------------- Old: ---- pycrdt-0.12.44.tar.xz New: ---- pycrdt-0.12.50.tar.xz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pycrdt.spec ++++++ --- /var/tmp/diff_new_pack.daQtsD/_old 2026-03-23 17:12:59.476736780 +0100 +++ /var/tmp/diff_new_pack.daQtsD/_new 2026-03-23 17:12:59.476736780 +0100 @@ -1,7 +1,7 @@ # # spec file for package python-pycrdt # -# Copyright (c) 2025 SUSE LLC and contributors +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -17,7 +17,7 @@ Name: python-pycrdt -Version: 0.12.44 +Version: 0.12.50 Release: 0 Summary: Python bindings for Yrs License: MIT @@ -31,12 +31,13 @@ BuildRequires: fdupes BuildRequires: python-rpm-macros Requires: (python-anyio >= 4.4 with python-anyio < 5) +Requires: (python-exceptiongroup >= 4.14.0 if python-base < 3.11) Requires: (python-typing_extensions >= 4.14.0 if python-base < 3.11) # SECTION test requirements BuildRequires: %{python_module pytest >= 8.3.5} BuildRequires: %{python_module anyio >= 4.4.0 with %python-anyio < 5} BuildRequires: %{python_module exceptiongroup if %python-base < 3.11} -BuildRequires: %{python_module trio >= 0.25.1 with %python-trio < 0.33} +BuildRequires: %{python_module trio >= 0.25.1 with %python-trio < 0.34} BuildRequires: %{python_module typing_extensions >= 4.14.0 if %python-base < 3.11} # /SECTION %python_subpackages ++++++ pycrdt-0.12.44.tar.xz -> pycrdt-0.12.50.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/.github/workflows/publish.yml new/pycrdt-0.12.50/.github/workflows/publish.yml --- old/pycrdt-0.12.44/.github/workflows/publish.yml 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/.github/workflows/publish.yml 2026-03-16 10:27:09.000000000 +0100 @@ -22,7 +22,7 @@ - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: "3.11" architecture: x64 - uses: dtolnay/rust-toolchain@stable - name: Build wheels - universal2 @@ -35,7 +35,7 @@ pip install pycrdt --no-deps --no-index --find-links dist --force-reinstall pytest --ignore tests/test_types.py - name: Upload wheels - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: wheels-macos path: dist @@ -70,7 +70,7 @@ pip install pycrdt --no-deps --no-index --find-links dist --force-reinstall pytest --ignore tests/test_types.py - name: Upload wheels - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: wheels-windows-${{ matrix.platform.target }} path: dist @@ -84,7 +84,7 @@ - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: "3.11" architecture: x64 - name: Build sdist if: ${{ matrix.target == 'x86_64' }} @@ -106,7 +106,7 @@ pip install pycrdt --no-deps --no-index --find-links dist --force-reinstall pytest --ignore tests/test_types.py - name: Upload wheels - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: wheels-linux-${{ matrix.target }} path: dist @@ -120,7 +120,7 @@ - uses: actions/checkout@v6 - uses: actions/setup-python@v6 with: - python-version: '3.11' + python-version: "3.11" - name: Build wheels uses: PyO3/maturin-action@v1 with: @@ -144,11 +144,87 @@ pytest --ignore tests/test_types.py - name: Upload wheels - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v7 with: name: wheels-linux-cross-${{ matrix.target }} path: dist + musllinux: + runs-on: ubuntu-latest + strategy: + matrix: + target: + - x86_64-unknown-linux-musl + - i686-unknown-linux-musl + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + architecture: x64 + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + rust-toolchain: stable + target: ${{ matrix.target }} + manylinux: musllinux_1_2 + args: --release --out dist -i 3.10 3.11 3.12 3.13 3.14 + - name: Test built wheel + if: matrix.target == 'x86_64-unknown-linux-musl' + run: | + docker run --rm -v "$(pwd):/workspace" -w /workspace alpine:latest sh -c " + apk add --no-cache python3 py3-pip && + pip3 install --break-system-packages -U pip pytest 'pydantic>=2.5.2,<3' 'anyio>=4.4.0,<5' 'trio>=0.25.1,<0.32' && + pip3 install --break-system-packages pycrdt --no-deps --no-index --find-links dist/ --force-reinstall && + pytest --ignore tests/test_types.py + " + - name: Upload wheels + uses: actions/upload-artifact@v7 + with: + name: wheels-musllinux-${{ matrix.target }} + path: dist + + musllinux-cross: + runs-on: ubuntu-latest + strategy: + matrix: + platform: + - target: aarch64-unknown-linux-musl + arch: aarch64 + - target: armv7-unknown-linux-musleabihf + arch: armv7 + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + rust-toolchain: stable + target: ${{ matrix.platform.target }} + manylinux: musllinux_1_2 + args: --release --out dist -i 3.10 3.11 3.12 3.13 3.14 + + - uses: uraimo/[email protected] + name: Test built wheel + with: + arch: ${{ matrix.platform.arch }} + distro: alpine_latest + githubToken: ${{ github.token }} + install: | + apk add --no-cache python3 py3-pip + pip3 install --break-system-packages -U pip pytest "pydantic>=2.5.2,<3" "anyio>=4.4.0,<5" "trio>=0.25.1,<0.32" + run: | + pip3 install --break-system-packages pycrdt --no-deps --no-index --find-links dist/ --force-reinstall + pytest --ignore tests/test_types.py + + - name: Upload wheels + uses: actions/upload-artifact@v7 + with: + name: wheels-musllinux-cross-${{ matrix.platform.target }} + path: dist + pypi-release: name: Publish to PyPI runs-on: ubuntu-latest @@ -157,11 +233,13 @@ - windows - linux - linux-cross + - musllinux + - musllinux-cross environment: release permissions: id-token: write steps: - - uses: actions/download-artifact@v6 + - uses: actions/download-artifact@v8 with: path: dist merge-multiple: true @@ -174,12 +252,12 @@ permissions: contents: write steps: - - uses: actions/checkout@v6 - - id: changelog - uses: agronholm/release-notes@v1 - with: - path: CHANGELOG.md - version_pattern: ^\#\# ([0-9][^*]*)\n - - uses: ncipollo/release-action@v1 - with: - body: ${{ steps.changelog.outputs.changelog }} + - uses: actions/checkout@v6 + - id: changelog + uses: agronholm/release-notes@v1 + with: + path: CHANGELOG.md + version_pattern: ^\#\# ([0-9][^*]*)\n + - uses: ncipollo/release-action@v1 + with: + body: ${{ steps.changelog.outputs.changelog }} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/.pre-commit-config.yaml new/pycrdt-0.12.50/.pre-commit-config.yaml --- old/pycrdt-0.12.44/.pre-commit-config.yaml 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/.pre-commit-config.yaml 2026-03-16 10:27:09.000000000 +0100 @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.7 + rev: v0.15.5 hooks: - id: ruff args: [--fix, --show-fixes] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/CHANGELOG.md new/pycrdt-0.12.50/CHANGELOG.md --- old/pycrdt-0.12.44/CHANGELOG.md 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/CHANGELOG.md 2026-03-16 10:27:09.000000000 +0100 @@ -1,5 +1,29 @@ # Version history +## 0.12.50 + +- Fix concurrent async transactions. + +## 0.12.49 + +- Remove use of cache for observer callbacks. + +## 0.12.48 + +- Fix tuple handling (convert to list). + +## 0.12.47 + +- Build wheels for `musllinux` platform. + +## 0.12.46 + +- Bump `pyo3` to v0.28.0. + +## 0.12.45 + +- Raise all exceptions from observer callbacks in an exception group. + ## 0.12.44 - Expose `UndoManager` stack items. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/Cargo.toml new/pycrdt-0.12.50/Cargo.toml --- old/pycrdt-0.12.44/Cargo.toml 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/Cargo.toml 2026-03-16 10:27:09.000000000 +0100 @@ -1,6 +1,6 @@ [package] name = "pycrdt" -version = "0.12.44" +version = "0.12.50" edition = "2021" [lib] @@ -8,6 +8,6 @@ crate-type = ["cdylib"] [dependencies] -serde_json = "1.0.140" +serde_json = "1.0.149" yrs = "0.25.0" -pyo3 = "0.27.1" +pyo3 = "0.28.2" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/pyproject.toml new/pycrdt-0.12.50/pyproject.toml --- old/pycrdt-0.12.44/pyproject.toml 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/pyproject.toml 2026-03-16 10:27:09.000000000 +0100 @@ -33,16 +33,16 @@ dependencies = [ "anyio >=4.4.0,<5.0.0", "typing_extensions >=4.14.0; python_version<'3.11'", + "exceptiongroup; python_version<'3.11'", ] [dependency-groups] test = [ "pytest >=8.3.5,<10", "anyio", - "trio >=0.25.1,<0.33", + "trio >=0.25.1,<0.34", "pydantic >=2.5.2,<3", "coverage[toml] >=7", - "exceptiongroup; python_version<'3.11'", ] types = [ "mypy >=1.19.0", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/python/pycrdt/_base.py new/pycrdt-0.12.50/python/pycrdt/_base.py --- old/pycrdt-0.12.44/python/pycrdt/_base.py 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/python/pycrdt/_base.py 2026-03-16 10:27:09.000000000 +0100 @@ -2,7 +2,7 @@ import threading from abc import ABC, abstractmethod -from functools import lru_cache, partial +from functools import partial from inspect import signature from types import UnionType from typing import ( @@ -47,6 +47,7 @@ _doc: _Doc _twin_doc: BaseDoc | None _txn: Transaction | None + _exceptions: list[Exception] _txn_lock: threading.Lock _txn_async_lock: anyio.Lock _allow_multithreading: bool @@ -70,6 +71,7 @@ doc = _Doc(client_id, skip_gc) self._doc = doc self._txn = None + self._exceptions = [] self._txn_lock = threading.Lock() self._txn_async_lock = anyio.Lock() self._Model = Model @@ -183,7 +185,8 @@ return self._type_name def observe(self, callback: Callable[[BaseEvent], None]) -> Subscription: - _callback = partial(observe_callback, callback, self.doc) + param_nb = len(signature(callback).parameters) + _callback = partial(observe_callback, callback, self.doc, param_nb) subscription = self.integrated.observe(_callback) self._subscriptions.append(subscription) return subscription @@ -195,7 +198,8 @@ Args: callback: The callback to call with the list of events. """ - _callback = partial(observe_deep_callback, callback, self.doc) + param_nb = len(signature(callback).parameters) + _callback = partial(observe_deep_callback, callback, self.doc, param_nb) subscription = self.integrated.observe_deep(_callback) self._subscriptions.append(subscription) return subscription @@ -296,26 +300,32 @@ def observe_callback( callback: Callable[[], None] | Callable[[Any], None] | Callable[[Any, ReadTransaction], None], doc: Doc, + param_nb: int, event: Any, ): - param_nb = count_parameters(callback) _event = event_types[type(event)](event, doc) with doc._read_transaction(event.transaction) as txn: params = (_event, txn) - callback(*params[:param_nb]) # type: ignore[arg-type] + try: + callback(*params[:param_nb]) # type: ignore[arg-type] + except Exception as exc: + doc._exceptions.append(exc) def observe_deep_callback( callback: Callable[[], None] | Callable[[Any], None] | Callable[[Any, ReadTransaction], None], doc: Doc, + param_nb: int, events: list[Any], ): - param_nb = count_parameters(callback) for idx, event in enumerate(events): events[idx] = event_types[type(event)](event, doc) with doc._read_transaction(event.transaction) as txn: params = (events, txn) - callback(*params[:param_nb]) # type: ignore[arg-type] + try: + callback(*params[:param_nb]) # type: ignore[arg-type] + except Exception as exc: + doc._exceptions.append(exc) class BaseEvent: @@ -356,12 +366,6 @@ return value -@lru_cache(maxsize=1024) -def count_parameters(func: Callable) -> int: - """Count the number of parameters in a callable""" - return len(signature(func).parameters) - - class Typed: _: Any diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/python/pycrdt/_doc.py new/pycrdt-0.12.50/python/pycrdt/_doc.py --- old/pycrdt-0.12.44/python/pycrdt/_doc.py 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/python/pycrdt/_doc.py 2026-03-16 10:27:09.000000000 +0100 @@ -326,7 +326,7 @@ if iscoroutinefunction(callback): cb = self._async_callback_to_sync(callback) else: - cb = cast(Callable[[TransactionEvent], None], callback) + cb = partial(observe_callback, cast(Callable[[TransactionEvent], None], callback), self) subscription = self._doc.observe(cb) self._subscriptions.append(subscription) return subscription @@ -358,7 +358,7 @@ if iscoroutinefunction(callback): cb = self._async_callback_to_sync(callback) else: - cb = cast(Callable[[SubdocsEvent], None], callback) + cb = partial(observe_callback, cast(Callable[[SubdocsEvent], None], callback), self) subscription = self._doc.observe_subdocs(cb) self._subscriptions.append(subscription) return subscription @@ -503,4 +503,15 @@ doc[name] = root_type +def observe_callback( + callback: Callable[[TransactionEvent], None] | Callable[[SubdocsEvent], None], + doc: Doc, + event: Any, +) -> None: + try: + callback(event) + except Exception as exc: + doc._exceptions.append(exc) + + base_types[_Doc] = Doc diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/python/pycrdt/_transaction.py new/pycrdt-0.12.50/python/pycrdt/_transaction.py --- old/pycrdt-0.12.44/python/pycrdt/_transaction.py 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/python/pycrdt/_transaction.py 2026-03-16 10:27:09.000000000 +0100 @@ -1,5 +1,6 @@ from __future__ import annotations +import sys from functools import partial from types import TracebackType from typing import TYPE_CHECKING, Any @@ -8,6 +9,9 @@ from ._pycrdt import Transaction as _Transaction +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup # pragma: no cover + if TYPE_CHECKING: from ._doc import Doc @@ -81,15 +85,20 @@ del self._doc._origins[origin_hash] if self._doc._allow_multithreading: self._doc._txn_lock.release() + if self._doc._exceptions: + exceptions = tuple(self._doc._exceptions) + self._doc._exceptions.clear() + raise ExceptionGroup("Observer callback error", exceptions) finally: self._txn.drop() self._txn = None self._doc._txn = None async def __aenter__(self, _acquire_transaction: bool = True) -> Transaction: - if self._leases > 0 and self._doc._task_group is None: + if self._leases == 0: + self._doc._task_group = await create_task_group().__aenter__() + elif self._doc._task_group is None: raise RuntimeError("Already in a non-async transaction") - self._doc._task_group = await create_task_group().__aenter__() return self.__enter__(_acquire_transaction) async def __aexit__( diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/src/type_conversions.rs new/pycrdt-0.12.50/src/type_conversions.rs --- old/pycrdt-0.12.44/src/type_conversions.rs 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/src/type_conversions.rs 2026-03-16 10:27:09.000000000 +0100 @@ -1,6 +1,6 @@ use pyo3::prelude::*; use pyo3::IntoPyObjectExt; -use pyo3::types::{PyAny, PyBool, PyByteArray, PyBytes, PyDict, PyFloat, PyIterator, PyList, PyInt, PyString}; +use pyo3::types::{PyAny, PyBool, PyByteArray, PyBytes, PyDict, PyFloat, PyIterator, PyList, PyInt, PyString, PyTuple}; use yrs::types::{Attrs, Change, EntryChange, Delta, Events, Path, PathSegment}; use yrs::{Any, Out, TransactionMut, XmlOut}; use std::collections::{VecDeque, HashMap}; @@ -240,6 +240,13 @@ let mut items = Vec::new(); for i in v.iter() { let a = py_to_any(&i); + items.push(a); + } + Any::Array(items.into()) + } else if let Ok(v) = value.cast::<PyTuple>() { + let mut items = Vec::new(); + for i in v.iter() { + let a = py_to_any(&i); items.push(a); } Any::Array(items.into()) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/tests/test_array.py new/pycrdt-0.12.50/tests/test_array.py --- old/pycrdt-0.12.44/tests/test_array.py 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/tests/test_array.py 2026-03-16 10:27:09.000000000 +0100 @@ -327,3 +327,31 @@ assert array1.to_py() in (first + second, second + first) new_idx = sticky_index.get_index() assert array1[new_idx] == "*" + + +def test_observer_exceptions(): + values = [] + + def callback0(event): + values.append("val0") + + def callback1(event): + values.append("val1") + raise RuntimeError("error1") + + def callback2(event): + values.append("val2") + raise ValueError("error2") + + doc = Doc() + array = doc.get("array", type=Array) + array.observe(callback0) + array.observe(callback1) + array.observe_deep(callback2) + + with pytest.RaisesGroup(RuntimeError, ValueError, match="Observer callback error") as exc_info: + array.append(0) + + assert exc_info.group_contains(RuntimeError, match="error1") + assert exc_info.group_contains(ValueError, match="error2") + assert set(values) == set(["val2", "val1", "val0"]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/tests/test_doc.py new/pycrdt-0.12.50/tests/test_doc.py --- old/pycrdt-0.12.44/tests/test_doc.py 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/tests/test_doc.py 2026-03-16 10:27:09.000000000 +0100 @@ -310,3 +310,60 @@ assert len(updates) == 2 assert updates[0].endswith(b"Hello\x00") assert updates[1].endswith(b", World!\x00") + + +def test_observer_exceptions(): + values = [] + + def callback0(event): + values.append("val0") + + def callback1(event): + values.append("val1") + raise RuntimeError("error1") + + def callback2(event): + values.append("val2") + raise ValueError("error2") + + doc = Doc() + text = doc.get("text", type=Text) + doc.observe(callback0) + doc.observe(callback1) + doc.observe(callback2) + + with pytest.RaisesGroup(RuntimeError, ValueError, match="Observer callback error") as exc_info: + text += "hello" + + assert exc_info.group_contains(RuntimeError, match="error1") + assert exc_info.group_contains(ValueError, match="error2") + assert values == ["val2", "val1", "val0"] + + +async def test_async_observer_exceptions(): + values = [] + + def callback0(event): + values.append("val0") + + async def callback1(event): + values.append("val1") + raise RuntimeError("error1") + + async def callback2(event): + values.append("val2") + raise ValueError("error2") + + doc = Doc() + text = doc.get("text", type=Text) + doc.observe(callback0) + doc.observe(callback1) + doc.observe(callback2) + + with pytest.RaisesGroup(RuntimeError, ValueError) as exc_info: + async with doc.transaction(): + text += "hello" + + assert exc_info.group_contains(RuntimeError, match="error1") + assert exc_info.group_contains(ValueError, match="error2") + assert set(values) == set(["val2", "val1", "val0"]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/tests/test_map.py new/pycrdt-0.12.50/tests/test_map.py --- old/pycrdt-0.12.44/tests/test_map.py 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/tests/test_map.py 2026-03-16 10:27:09.000000000 +0100 @@ -197,3 +197,51 @@ assert len(keys) == 2 assert keys[0] == {"key0": {"action": "add", "newValue": "Hello"}} assert keys[1] == {"key1": {"action": "add", "newValue": ", World!"}} + + +def test_tuple_handling(): + """Test that tuples in nested dictionaries are properly preserved. + + Related to issue #368 where tuples in nested dictionaries were being + converted to None values. + """ + doc = Doc() + map0 = doc.get("map0", type=Map) + + # Test basic tuple handling + test_data = { + "simple_tuple": (1, 2, 3), + "mixed_tuple": (1, "hello", True, None), + "empty_tuple": (), + "single_tuple": (42,), + "nested_dict": {"position": (5, 0), "range": (10, 20), "content": "hello"}, + } + + map0.update(test_data) + + result = map0.to_py() + + # Tuples should be converted to lists + assert result["simple_tuple"] == [1.0, 2.0, 3.0] + assert result["mixed_tuple"] == [1.0, "hello", True, None] + assert result["empty_tuple"] == [] + assert result["single_tuple"] == [42.0] + + # Nested dictionary with tuples + nested = result["nested_dict"] + assert nested["position"] == [5.0, 0.0] + assert nested["range"] == [10.0, 20.0] + assert nested["content"] == "hello" + + # Test direct assignment + map0["direct_tuple"] = (100, 200) + assert map0["direct_tuple"] == [100.0, 200.0] + + # Test the original issue case + selection_data = {"selection": {"start": (5, 0), "end": (10, 0), "content": "hello"}} + map0.update(selection_data) + selection_result = map0["selection"] + + assert selection_result["start"] == [5.0, 0.0] + assert selection_result["end"] == [10.0, 0.0] + assert selection_result["content"] == "hello" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/tests/test_observe_gc.py new/pycrdt-0.12.50/tests/test_observe_gc.py --- old/pycrdt-0.12.44/tests/test_observe_gc.py 1970-01-01 01:00:00.000000000 +0100 +++ new/pycrdt-0.12.50/tests/test_observe_gc.py 2026-03-16 10:27:09.000000000 +0100 @@ -0,0 +1,26 @@ +import gc +import weakref + +import pytest +from pycrdt import Doc, Map + + [email protected]("method", ["observe", "observe_deep"]) +def test_observe_bound_method_gc(method): + """Bound method callbacks should be garbage collected after unobserve().""" + + class Observer: + def on_change(self, event): + pass + + doc = Doc() + map_ = doc.get("map", type=Map) + observer = Observer() + freed = [] + weakref.finalize(observer, freed.append, True) + sub = getattr(map_, method)(observer.on_change) + map_["key"] = "value" + map_.unobserve(sub) + del observer + gc.collect() + assert freed, "Observer was not garbage collected after unobserve()" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pycrdt-0.12.44/tests/test_transaction.py new/pycrdt-0.12.50/tests/test_transaction.py --- old/pycrdt-0.12.44/tests/test_transaction.py 2025-12-04 16:06:28.000000000 +0100 +++ new/pycrdt-0.12.50/tests/test_transaction.py 2026-03-16 10:27:09.000000000 +0100 @@ -359,3 +359,16 @@ async with doc.transaction(): pass # pragma: nocover assert str(excinfo.value) == "Already in a non-async transaction" + + +async def test_concurrent_async_transactions(): + doc = Doc() + + async def create_async_transaction(seconds, task_status): + async with doc.transaction(): + task_status.started() + await sleep(seconds) + + async with create_task_group() as tg: + await tg.start(create_async_transaction, 0.2) + await tg.start(create_async_transaction, 0.1) ++++++ pycrdt.obsinfo ++++++ --- /var/tmp/diff_new_pack.daQtsD/_old 2026-03-23 17:12:59.664744609 +0100 +++ /var/tmp/diff_new_pack.daQtsD/_new 2026-03-23 17:12:59.668744777 +0100 @@ -1,5 +1,5 @@ name: pycrdt -version: 0.12.44 -mtime: 1764860788 -commit: 58633de1873b21512eb97ffdc355a7e23ecda5c4 +version: 0.12.50 +mtime: 1773653229 +commit: da89d252dd37bde63774a7efc654f8c0cf742f6c ++++++ vendor.tar.xz ++++++ ++++ 1020629 lines of diff (skipped)
