pycrdt 0.12.37__tar.gz → 0.12.40__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pycrdt might be problematic. Click here for more details.

Files changed (66) hide show
  1. {pycrdt-0.12.37 → pycrdt-0.12.40}/.github/workflows/publish.yml +5 -5
  2. {pycrdt-0.12.37 → pycrdt-0.12.40}/.github/workflows/test.yml +2 -0
  3. {pycrdt-0.12.37 → pycrdt-0.12.40}/CHANGELOG.md +9 -0
  4. {pycrdt-0.12.37 → pycrdt-0.12.40}/Cargo.lock +17 -76
  5. {pycrdt-0.12.37 → pycrdt-0.12.40}/Cargo.toml +1 -1
  6. {pycrdt-0.12.37 → pycrdt-0.12.40}/PKG-INFO +1 -1
  7. {pycrdt-0.12.37 → pycrdt-0.12.40}/docs/usage.md +32 -2
  8. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_base.py +9 -4
  9. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_provider.py +2 -4
  10. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_transaction.py +12 -10
  11. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/doc.rs +16 -9
  12. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/map.rs +19 -18
  13. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/transaction.rs +6 -1
  14. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_doc.py +19 -0
  15. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_transaction.py +2 -4
  16. {pycrdt-0.12.37 → pycrdt-0.12.40}/.gitignore +0 -0
  17. {pycrdt-0.12.37 → pycrdt-0.12.40}/.pre-commit-config.yaml +0 -0
  18. {pycrdt-0.12.37 → pycrdt-0.12.40}/LICENSE +0 -0
  19. {pycrdt-0.12.37 → pycrdt-0.12.40}/README.md +0 -0
  20. {pycrdt-0.12.37 → pycrdt-0.12.40}/docs/api_reference.md +0 -0
  21. {pycrdt-0.12.37 → pycrdt-0.12.40}/docs/assets/logo.png +0 -0
  22. {pycrdt-0.12.37 → pycrdt-0.12.40}/docs/index.md +0 -0
  23. {pycrdt-0.12.37 → pycrdt-0.12.40}/docs/install.md +0 -0
  24. {pycrdt-0.12.37 → pycrdt-0.12.40}/mkdocs.yml +0 -0
  25. {pycrdt-0.12.37 → pycrdt-0.12.40}/pyproject.toml +0 -0
  26. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/__init__.py +0 -0
  27. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_array.py +0 -0
  28. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_awareness.py +0 -0
  29. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_doc.py +0 -0
  30. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_map.py +0 -0
  31. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_pycrdt.pyi +0 -0
  32. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_snapshot.py +0 -0
  33. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_sticky_index.py +0 -0
  34. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_sync.py +0 -0
  35. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_text.py +0 -0
  36. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_undo.py +0 -0
  37. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_update.py +0 -0
  38. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_version.py +0 -0
  39. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_xml.py +0 -0
  40. {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/py.typed +0 -0
  41. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/array.rs +0 -0
  42. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/lib.rs +0 -0
  43. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/snapshot.rs +0 -0
  44. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/sticky_index.rs +0 -0
  45. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/subscription.rs +0 -0
  46. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/text.rs +0 -0
  47. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/type_conversions.rs +0 -0
  48. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/undo.rs +0 -0
  49. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/update.rs +0 -0
  50. {pycrdt-0.12.37 → pycrdt-0.12.40}/src/xml.rs +0 -0
  51. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/conftest.py +0 -0
  52. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_array.py +0 -0
  53. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_awareness.py +0 -0
  54. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_map.py +0 -0
  55. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_model.py +0 -0
  56. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_provider.py +0 -0
  57. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_snapshot.py +0 -0
  58. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_sync.py +0 -0
  59. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_text.py +0 -0
  60. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_threads.py +0 -0
  61. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_typed.py +0 -0
  62. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_types.py +0 -0
  63. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_undo.py +0 -0
  64. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_update.py +0 -0
  65. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_xml.py +0 -0
  66. {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/utils.py +0 -0
@@ -28,7 +28,7 @@ jobs:
28
28
  - name: Build wheels - universal2
29
29
  uses: PyO3/maturin-action@v1
30
30
  with:
31
- args: --release --target universal2-apple-darwin --out dist -i 3.10 3.11 3.12 3.13 pypy3.10
31
+ args: --release --target universal2-apple-darwin --out dist -i 3.10 3.11 3.12 3.13 3.14 pypy3.10 pypy3.11
32
32
  - name: Test built wheel - universal2
33
33
  run: |
34
34
  pip install pytest pytest-mypy-testing "pydantic>=2.5.2,<3" "anyio>=4.4.0,<5" "trio>=0.25.1,<0.32" "exceptiongroup; python_version<'3.11'"
@@ -47,9 +47,9 @@ jobs:
47
47
  matrix:
48
48
  platform:
49
49
  - target: x64
50
- interpreter: ["3.10", "3.11", "3.12", "3.13"]
50
+ interpreter: ["3.10", "3.11", "3.12", "3.13", "3.14"]
51
51
  - target: x86
52
- interpreter: ["3.10", "3.11", "3.12", "3.13"]
52
+ interpreter: ["3.10", "3.11", "3.12", "3.13", "3.14"]
53
53
  steps:
54
54
  - uses: actions/checkout@v4
55
55
  - uses: actions/setup-python@v5
@@ -98,7 +98,7 @@ jobs:
98
98
  rust-toolchain: stable
99
99
  target: ${{ matrix.target }}
100
100
  manylinux: auto
101
- args: --release --out dist -i 3.10 3.11 3.12 3.13 pypy3.10
101
+ args: --release --out dist -i 3.10 3.11 3.12 3.13 3.14 pypy3.10 pypy3.11
102
102
  - name: Test built wheel
103
103
  if: matrix.target == 'x86_64'
104
104
  run: |
@@ -127,7 +127,7 @@ jobs:
127
127
  rust-toolchain: stable
128
128
  target: ${{ matrix.target }}
129
129
  manylinux: auto
130
- args: --release --out dist -i 3.10 3.11 3.12 3.13 pypy3.10
130
+ args: --release --out dist -i 3.10 3.11 3.12 3.13 3.14 pypy3.10 pypy3.11
131
131
 
132
132
  - uses: uraimo/run-on-arch-action@v2.8.1
133
133
  name: Test built wheel
@@ -25,7 +25,9 @@ jobs:
25
25
  - '3.11'
26
26
  - '3.12'
27
27
  - '3.13'
28
+ - '3.14'
28
29
  - 'pypy3.10'
30
+ - 'pypy3.11'
29
31
 
30
32
  runs-on: ${{ matrix.os }}-latest
31
33
 
@@ -1,5 +1,14 @@
1
1
  # Version history
2
2
 
3
+ ## 0.12.40
4
+
5
+ - Support Python v3.14.
6
+
7
+ ## 0.12.38
8
+
9
+ - Improve error propagation with `PyResult`.
10
+ - Cleanup transaction when commit fails.
11
+
3
12
  ## 0.12.37
4
13
 
5
14
  - Drop Python 3.9 support.
@@ -168,11 +168,10 @@ checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
168
168
 
169
169
  [[package]]
170
170
  name = "lock_api"
171
- version = "0.4.13"
171
+ version = "0.4.14"
172
172
  source = "registry+https://github.com/rust-lang/crates.io-index"
173
- checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
173
+ checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
174
174
  dependencies = [
175
- "autocfg",
176
175
  "scopeguard",
177
176
  ]
178
177
 
@@ -211,15 +210,15 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
211
210
 
212
211
  [[package]]
213
212
  name = "parking_lot_core"
214
- version = "0.9.11"
213
+ version = "0.9.12"
215
214
  source = "registry+https://github.com/rust-lang/crates.io-index"
216
- checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
215
+ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
217
216
  dependencies = [
218
217
  "cfg-if",
219
218
  "libc",
220
219
  "redox_syscall",
221
220
  "smallvec",
222
- "windows-targets",
221
+ "windows-link",
223
222
  ]
224
223
 
225
224
  [[package]]
@@ -245,7 +244,7 @@ dependencies = [
245
244
 
246
245
  [[package]]
247
246
  name = "pycrdt"
248
- version = "0.12.37"
247
+ version = "0.12.40"
249
248
  dependencies = [
250
249
  "pyo3",
251
250
  "serde_json",
@@ -315,18 +314,18 @@ dependencies = [
315
314
 
316
315
  [[package]]
317
316
  name = "quote"
318
- version = "1.0.40"
317
+ version = "1.0.41"
319
318
  source = "registry+https://github.com/rust-lang/crates.io-index"
320
- checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
319
+ checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
321
320
  dependencies = [
322
321
  "proc-macro2",
323
322
  ]
324
323
 
325
324
  [[package]]
326
325
  name = "redox_syscall"
327
- version = "0.5.17"
326
+ version = "0.5.18"
328
327
  source = "registry+https://github.com/rust-lang/crates.io-index"
329
- checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
328
+ checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
330
329
  dependencies = [
331
330
  "bitflags",
332
331
  ]
@@ -426,18 +425,18 @@ checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
426
425
 
427
426
  [[package]]
428
427
  name = "thiserror"
429
- version = "2.0.16"
428
+ version = "2.0.17"
430
429
  source = "registry+https://github.com/rust-lang/crates.io-index"
431
- checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0"
430
+ checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
432
431
  dependencies = [
433
432
  "thiserror-impl",
434
433
  ]
435
434
 
436
435
  [[package]]
437
436
  name = "thiserror-impl"
438
- version = "2.0.16"
437
+ version = "2.0.17"
439
438
  source = "registry+https://github.com/rust-lang/crates.io-index"
440
- checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960"
439
+ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
441
440
  dependencies = [
442
441
  "proc-macro2",
443
442
  "quote",
@@ -522,68 +521,10 @@ dependencies = [
522
521
  ]
523
522
 
524
523
  [[package]]
525
- name = "windows-targets"
526
- version = "0.52.6"
527
- source = "registry+https://github.com/rust-lang/crates.io-index"
528
- checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
529
- dependencies = [
530
- "windows_aarch64_gnullvm",
531
- "windows_aarch64_msvc",
532
- "windows_i686_gnu",
533
- "windows_i686_gnullvm",
534
- "windows_i686_msvc",
535
- "windows_x86_64_gnu",
536
- "windows_x86_64_gnullvm",
537
- "windows_x86_64_msvc",
538
- ]
539
-
540
- [[package]]
541
- name = "windows_aarch64_gnullvm"
542
- version = "0.52.6"
543
- source = "registry+https://github.com/rust-lang/crates.io-index"
544
- checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
545
-
546
- [[package]]
547
- name = "windows_aarch64_msvc"
548
- version = "0.52.6"
549
- source = "registry+https://github.com/rust-lang/crates.io-index"
550
- checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
551
-
552
- [[package]]
553
- name = "windows_i686_gnu"
554
- version = "0.52.6"
555
- source = "registry+https://github.com/rust-lang/crates.io-index"
556
- checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
557
-
558
- [[package]]
559
- name = "windows_i686_gnullvm"
560
- version = "0.52.6"
561
- source = "registry+https://github.com/rust-lang/crates.io-index"
562
- checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
563
-
564
- [[package]]
565
- name = "windows_i686_msvc"
566
- version = "0.52.6"
567
- source = "registry+https://github.com/rust-lang/crates.io-index"
568
- checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
569
-
570
- [[package]]
571
- name = "windows_x86_64_gnu"
572
- version = "0.52.6"
573
- source = "registry+https://github.com/rust-lang/crates.io-index"
574
- checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
575
-
576
- [[package]]
577
- name = "windows_x86_64_gnullvm"
578
- version = "0.52.6"
579
- source = "registry+https://github.com/rust-lang/crates.io-index"
580
- checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
581
-
582
- [[package]]
583
- name = "windows_x86_64_msvc"
584
- version = "0.52.6"
524
+ name = "windows-link"
525
+ version = "0.2.1"
585
526
  source = "registry+https://github.com/rust-lang/crates.io-index"
586
- checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
527
+ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
587
528
 
588
529
  [[package]]
589
530
  name = "yrs"
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pycrdt"
3
- version = "0.12.37"
3
+ version = "0.12.40"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycrdt
3
- Version: 0.12.37
3
+ Version: 0.12.40
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -88,13 +88,23 @@ CRDTs ensure that documents don't diverge, their shared documents will eventuall
88
88
 
89
89
  Every change to a shared data happens in a document transaction, and there can only be one transaction at a time. Pycrdt offers two methods for creating transactions:
90
90
 
91
- - `doc.transaction()`: used with a context manager, this will create a new transaction if there is no current one, or use the current transaction. This method will never block, and should be used most of the time.
91
+ - `doc.transaction()`: used with a context manager or an async context manager, this will create a new transaction if there is no current one, or use the current transaction. This method will never block, and should be used most of the time.
92
92
  - `doc.new_transaction()`: used with a context manager or an async context manager, this will always try to create a new transaction. This method can block, waiting for a transaction to be released.
93
93
 
94
94
  ### Non-blocking transactions
95
95
 
96
- When no current transaction exists, an implicit transaction is created.
96
+ #### Synchronous context manager
97
+
98
+ When no current transaction exists, an implicit transaction is created:
99
+
100
+ ```py
101
+ doc = Doc()
102
+ text = doc.get("text", type=Text)
103
+ text += "This change creates an implicit transaction"
104
+ ```
105
+
97
106
  Grouping multiple changes in a single transaction makes them atomic: they will appear as done simultaneously rather than sequentially.
107
+ For performances, it is always better to try and group multiple changes in a single transaction.
98
108
 
99
109
  ```py
100
110
  with doc.transaction():
@@ -115,6 +125,26 @@ with doc.transaction() as t0:
115
125
  map0["key1"] = "value1"
116
126
  ```
117
127
 
128
+ #### Asynchronous context manager
129
+
130
+ Transactions created with an async context manager allow to register async callbacks when [observing document changes](#document-events). This makes
131
+ it possible to apply back-pressure on document changes. With just synchronous callbacks, lots of document changes could be done
132
+ without giving a chance to the event loop to send them over the wire, for instance. In the following example, the async context manager
133
+ will not exit until the async callback is done:
134
+
135
+ ```py
136
+ async def async_callback(event):
137
+ await send(event.update)
138
+
139
+ doc.observe(async_callback)
140
+
141
+ async with doc.transaction():
142
+ ...
143
+ ```
144
+
145
+ Note that registering an async callback and creating a transaction with a synchronous context manager will result in an error.
146
+ Also, it is not possible to have a nested async transaction while the root transaction is synchronous.
147
+
118
148
  ### Blocking transactions
119
149
 
120
150
  #### Multithreading
@@ -4,6 +4,7 @@ import threading
4
4
  from abc import ABC, abstractmethod
5
5
  from functools import lru_cache, partial
6
6
  from inspect import signature
7
+ from types import UnionType
7
8
  from typing import (
8
9
  TYPE_CHECKING,
9
10
  Any,
@@ -12,6 +13,8 @@ from typing import (
12
13
  Type,
13
14
  Union,
14
15
  cast,
16
+ get_args,
17
+ get_origin,
15
18
  get_type_hints,
16
19
  overload,
17
20
  )
@@ -388,10 +391,12 @@ class Typed:
388
391
  if key not in annotations:
389
392
  raise AttributeError(f'"{type(self).mro()[0]}" has no attribute "{key}"')
390
393
  expected_type = annotations[key]
391
- if hasattr(expected_type, "__origin__"):
392
- expected_type = expected_type.__origin__
393
- if hasattr(expected_type, "__args__"):
394
- expected_types = expected_type.__args__
394
+ origin = get_origin(expected_type)
395
+ if origin in (Union, UnionType):
396
+ expected_types = get_args(expected_type)
397
+ elif origin is not None:
398
+ expected_type = origin
399
+ expected_types = (expected_type,)
395
400
  else:
396
401
  expected_types = (expected_type,)
397
402
  if type(value) not in expected_types:
@@ -141,10 +141,8 @@ class Provider:
141
141
  return self
142
142
 
143
143
  async def __aexit__(self, exc_type, exc_value, exc_tb):
144
- try:
145
- await self.stop()
146
- finally:
147
- return await self._exit_stack.__aexit__(exc_type, exc_value, exc_tb)
144
+ await self.stop()
145
+ return await self._exit_stack.__aexit__(exc_type, exc_value, exc_tb)
148
146
 
149
147
  @asynccontextmanager
150
148
  async def _get_or_create_task_group(self) -> AsyncIterator[TaskGroup]:
@@ -73,16 +73,18 @@ class Transaction:
73
73
  # since nested transactions reuse the root transaction
74
74
  if self._leases == 0:
75
75
  assert self._txn is not None
76
- if not isinstance(self, ReadTransaction):
77
- self._txn.commit()
78
- origin_hash = self._txn.origin()
79
- if origin_hash is not None:
80
- del self._doc._origins[origin_hash]
81
- if self._doc._allow_multithreading:
82
- self._doc._txn_lock.release()
83
- self._txn.drop()
84
- self._txn = None
85
- self._doc._txn = None
76
+ try:
77
+ if not isinstance(self, ReadTransaction):
78
+ self._txn.commit()
79
+ origin_hash = self._txn.origin()
80
+ if origin_hash is not None:
81
+ del self._doc._origins[origin_hash]
82
+ if self._doc._allow_multithreading:
83
+ self._doc._txn_lock.release()
84
+ finally:
85
+ self._txn.drop()
86
+ self._txn = None
87
+ self._doc._txn = None
86
88
 
87
89
  async def __aenter__(self, _acquire_transaction: bool = True) -> Transaction:
88
90
  if self._leases > 0 and self._doc._task_group is None:
@@ -68,18 +68,24 @@ impl Doc {
68
68
  #[pymethods]
69
69
  impl Doc {
70
70
  #[new]
71
- fn new(client_id: &Bound<'_, PyAny>, skip_gc: &Bound<'_, PyAny>) -> Self {
71
+ fn new(client_id: &Bound<'_, PyAny>, skip_gc: &Bound<'_, PyAny>) -> PyResult<Self> {
72
72
  let mut options = Options::default();
73
73
  if !client_id.is_none() {
74
- let _client_id: u64 = client_id.downcast::<PyInt>().unwrap().extract().unwrap();
74
+ let _client_id: u64 = client_id.downcast::<PyInt>()
75
+ .map_err(|_| PyValueError::new_err("client_id must be an integer"))?
76
+ .extract()
77
+ .map_err(|_| PyValueError::new_err("client_id must be a valid u64"))?;
75
78
  options.client_id = _client_id;
76
79
  }
77
80
  if !skip_gc.is_none() {
78
- let _skip_gc: bool = skip_gc.downcast::<PyBool>().unwrap().extract().unwrap();
81
+ let _skip_gc: bool = skip_gc.downcast::<PyBool>()
82
+ .map_err(|_| PyValueError::new_err("skip_gc must be a boolean"))?
83
+ .extract()
84
+ .map_err(|_| PyValueError::new_err("skip_gc must be a valid bool"))?;
79
85
  options.skip_gc = _skip_gc;
80
86
  }
81
87
  let doc = _Doc::with_options(options);
82
- Doc { doc }
88
+ Ok(Doc { doc })
83
89
  }
84
90
 
85
91
  #[staticmethod]
@@ -161,7 +167,8 @@ impl Doc {
161
167
  }
162
168
 
163
169
  fn apply_update(&mut self, txn: &mut Transaction, update: &Bound<'_, PyBytes>) -> PyResult<()> {
164
- let u = Update::decode_v1(update.as_bytes()).unwrap();
170
+ let u = Update::decode_v1(update.as_bytes())
171
+ .map_err(|e| PyValueError::new_err(format!("Cannot decode update: {}", e)))?;
165
172
  let mut _t = txn.transaction();
166
173
  let t = _t.as_mut().unwrap().as_mut();
167
174
  t.apply_update(u)
@@ -251,13 +258,13 @@ impl TransactionEvent {
251
258
  #[pymethods]
252
259
  impl TransactionEvent {
253
260
  #[getter]
254
- pub fn transaction<'py>(&mut self, py: Python<'py>) -> Bound<'py, PyAny> {
261
+ pub fn transaction<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
255
262
  if let Some(transaction) = &self.transaction {
256
- transaction.clone_ref(py).into_bound(py)
263
+ Ok(transaction.clone_ref(py).into_bound(py))
257
264
  } else {
258
- let transaction = Transaction::from(self.txn()).into_bound_py_any(py).unwrap();
265
+ let transaction = Transaction::from(self.txn()).into_bound_py_any(py)?;
259
266
  self.transaction = Some(transaction.clone().unbind());
260
- transaction
267
+ Ok(transaction)
261
268
  }
262
269
  }
263
270
 
@@ -94,7 +94,8 @@ impl Map {
94
94
  fn insert_doc(&self, txn: &mut Transaction, key: &str, doc: &Bound<'_, PyAny>) -> PyResult<()> {
95
95
  let mut _t = txn.transaction();
96
96
  let mut t = _t.as_mut().unwrap().as_mut();
97
- let d1: Doc = doc.extract().unwrap();
97
+ let d1: Doc = doc.extract()
98
+ .map_err(|_| PyTypeError::new_err("Expected Doc object"))?;
98
99
  let d2: _Doc = d1.doc;
99
100
  let doc_ref = self.map.insert(&mut t, key, d2);
100
101
  doc_ref.load(t);
@@ -215,24 +216,24 @@ impl MapEvent {
215
216
  #[pymethods]
216
217
  impl MapEvent {
217
218
  #[getter]
218
- pub fn transaction<'py>(&mut self, py: Python<'py>) -> Bound<'py, PyAny> {
219
+ pub fn transaction<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
219
220
  if let Some(transaction) = &self.transaction {
220
- transaction.clone_ref(py).into_bound(py)
221
+ Ok(transaction.clone_ref(py).into_bound(py))
221
222
  } else {
222
- let transaction = Transaction::from(self.txn()).into_bound_py_any(py).unwrap();
223
+ let transaction = Transaction::from(self.txn()).into_bound_py_any(py)?;
223
224
  self.transaction = Some(transaction.clone().unbind());
224
- transaction
225
+ Ok(transaction)
225
226
  }
226
227
  }
227
228
 
228
229
  #[getter]
229
- pub fn target<'py>(&mut self, py: Python<'py>) -> Bound<'py, PyAny> {
230
+ pub fn target<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
230
231
  if let Some(target) = &self.target {
231
- target.clone_ref(py).into_bound(py)
232
+ Ok(target.clone_ref(py).into_bound(py))
232
233
  } else {
233
- let target = Map::from(self.event().target().clone()).into_bound_py_any(py).unwrap();
234
+ let target = Map::from(self.event().target().clone()).into_bound_py_any(py)?;
234
235
  self.target = Some(target.clone().unbind());
235
- target
236
+ Ok(target)
236
237
  }
237
238
  }
238
239
 
@@ -248,9 +249,9 @@ impl MapEvent {
248
249
  }
249
250
 
250
251
  #[getter]
251
- pub fn keys<'py>(&mut self, py: Python<'py>) -> Bound<'py, PyAny> {
252
+ pub fn keys<'py>(&mut self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
252
253
  if let Some(keys) = &self.keys {
253
- keys.clone_ref(py).into_bound(py)
254
+ Ok(keys.clone_ref(py).into_bound(py))
254
255
  } else {
255
256
  let keys = {
256
257
  let keys = self.event().keys(self.txn());
@@ -258,20 +259,20 @@ impl MapEvent {
258
259
  for (key, value) in keys.iter() {
259
260
  let key = &**key;
260
261
  let value = EntryChangeWrapper(value);
261
- result.set_item(key, value.into_pyobject(py).unwrap()).unwrap();
262
+ result.set_item(key, value.into_pyobject(py)?)?;
262
263
  }
263
264
  result
264
265
  };
265
- let keys = keys.into_bound_py_any(py).unwrap();
266
+ let keys = keys.into_bound_py_any(py)?;
266
267
  self.keys = Some(keys.clone().unbind());
267
- keys
268
+ Ok(keys)
268
269
  }
269
270
  }
270
271
 
271
- fn __repr__(&mut self, py: Python<'_>) -> String {
272
- let target = self.target(py);
273
- let keys = self.keys(py);
272
+ fn __repr__(&mut self, py: Python<'_>) -> PyResult<String> {
273
+ let target = self.target(py)?;
274
+ let keys = self.keys(py)?;
274
275
  let path = self.path(py);
275
- format!("MapEvent(target={target}, keys={keys}, path={path})")
276
+ Ok(format!("MapEvent(target={target}, keys={keys}, path={path})"))
276
277
  }
277
278
  }
@@ -52,8 +52,13 @@ impl Transaction {
52
52
 
53
53
  #[pymethods]
54
54
  impl Transaction {
55
- pub fn commit(&mut self) {
55
+ pub fn commit(&mut self, py: Python<'_>) -> PyResult<()> {
56
56
  self.transaction().as_mut().unwrap().as_mut().commit();
57
+ // Check if any Python exception was raised during commit (e.g., in callbacks)
58
+ if let Some(err) = pyo3::PyErr::take(py) {
59
+ return Err(err);
60
+ }
61
+ Ok(())
57
62
  }
58
63
 
59
64
  pub fn drop(&self) {
@@ -231,6 +231,25 @@ def test_get_update_exception():
231
231
  assert str(excinfo.value) == "Cannot decode state"
232
232
 
233
233
 
234
+ def test_apply_update_exception():
235
+ doc = Doc()
236
+ with pytest.raises(ValueError) as excinfo:
237
+ doc.apply_update(b"\xFF\xFF\xFF\xFF")
238
+ assert "Cannot decode update" in str(excinfo.value)
239
+
240
+
241
+ def test_invalid_client_id():
242
+ with pytest.raises(ValueError) as excinfo:
243
+ Doc(client_id="not_an_int")
244
+ assert "client_id must be" in str(excinfo.value)
245
+
246
+
247
+ def test_invalid_skip_gc():
248
+ with pytest.raises(ValueError) as excinfo:
249
+ Doc(skip_gc="not_a_bool")
250
+ assert "skip_gc must be" in str(excinfo.value)
251
+
252
+
234
253
  async def test_iterate_events():
235
254
  doc = Doc()
236
255
  updates = []
@@ -326,12 +326,10 @@ async def test_async_callback_in_sync_transaction():
326
326
  doc.observe(async_callback)
327
327
  text = doc.get("text", type=Text)
328
328
 
329
- with pytest.raises(SystemError) as excinfo:
329
+ with pytest.raises(RuntimeError) as excinfo:
330
330
  with doc.transaction():
331
331
  text += "hello"
332
- if platform.python_implementation() != "PyPy": # pragma: nocover
333
- assert isinstance(excinfo.value.__context__, RuntimeError)
334
- assert str(excinfo.value.__context__) == "Async callback in non-async transaction"
332
+ assert str(excinfo.value) == "Async callback in non-async transaction"
335
333
 
336
334
 
337
335
  async def test_async_transaction_in_existing_async_transaction():
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes