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)

Reply via email to