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.
- {pycrdt-0.12.37 → pycrdt-0.12.40}/.github/workflows/publish.yml +5 -5
- {pycrdt-0.12.37 → pycrdt-0.12.40}/.github/workflows/test.yml +2 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/CHANGELOG.md +9 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/Cargo.lock +17 -76
- {pycrdt-0.12.37 → pycrdt-0.12.40}/Cargo.toml +1 -1
- {pycrdt-0.12.37 → pycrdt-0.12.40}/PKG-INFO +1 -1
- {pycrdt-0.12.37 → pycrdt-0.12.40}/docs/usage.md +32 -2
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_base.py +9 -4
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_provider.py +2 -4
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_transaction.py +12 -10
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/doc.rs +16 -9
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/map.rs +19 -18
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/transaction.rs +6 -1
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_doc.py +19 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_transaction.py +2 -4
- {pycrdt-0.12.37 → pycrdt-0.12.40}/.gitignore +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/.pre-commit-config.yaml +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/LICENSE +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/README.md +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/docs/api_reference.md +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/docs/assets/logo.png +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/docs/index.md +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/docs/install.md +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/mkdocs.yml +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/pyproject.toml +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/__init__.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_array.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_awareness.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_doc.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_map.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_pycrdt.pyi +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_snapshot.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_sticky_index.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_sync.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_text.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_undo.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_update.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_version.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/_xml.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/python/pycrdt/py.typed +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/array.rs +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/lib.rs +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/snapshot.rs +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/sticky_index.rs +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/subscription.rs +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/text.rs +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/type_conversions.rs +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/undo.rs +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/update.rs +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/src/xml.rs +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/conftest.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_array.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_awareness.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_map.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_model.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_provider.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_snapshot.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_sync.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_text.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_threads.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_typed.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_types.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_undo.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_update.py +0 -0
- {pycrdt-0.12.37 → pycrdt-0.12.40}/tests/test_xml.py +0 -0
- {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
|
|
@@ -168,11 +168,10 @@ checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174"
|
|
|
168
168
|
|
|
169
169
|
[[package]]
|
|
170
170
|
name = "lock_api"
|
|
171
|
-
version = "0.4.
|
|
171
|
+
version = "0.4.14"
|
|
172
172
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
173
|
-
checksum = "
|
|
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.
|
|
213
|
+
version = "0.9.12"
|
|
215
214
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
216
|
-
checksum = "
|
|
215
|
+
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
|
217
216
|
dependencies = [
|
|
218
217
|
"cfg-if",
|
|
219
218
|
"libc",
|
|
220
219
|
"redox_syscall",
|
|
221
220
|
"smallvec",
|
|
222
|
-
"windows-
|
|
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.
|
|
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.
|
|
317
|
+
version = "1.0.41"
|
|
319
318
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
320
|
-
checksum = "
|
|
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.
|
|
326
|
+
version = "0.5.18"
|
|
328
327
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
329
|
-
checksum = "
|
|
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.
|
|
428
|
+
version = "2.0.17"
|
|
430
429
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
431
|
-
checksum = "
|
|
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.
|
|
437
|
+
version = "2.0.17"
|
|
439
438
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
440
|
-
checksum = "
|
|
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-
|
|
526
|
-
version = "0.
|
|
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 = "
|
|
527
|
+
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
|
587
528
|
|
|
588
529
|
[[package]]
|
|
589
530
|
name = "yrs"
|
|
@@ -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
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
77
|
-
self
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
self._doc.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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>()
|
|
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>()
|
|
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())
|
|
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)
|
|
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()
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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(
|
|
329
|
+
with pytest.raises(RuntimeError) as excinfo:
|
|
330
330
|
with doc.transaction():
|
|
331
331
|
text += "hello"
|
|
332
|
-
|
|
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
|
|
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
|