Skip to content

Commit

Permalink
Add API reference
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbrochart committed Sep 27, 2024
1 parent f37f4f0 commit 9b32184
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 13 deletions.
9 changes: 9 additions & 0 deletions docs/api_reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# API reference

::: pycrdt
options:
members:
- Doc
- Subscription
- SubdocsEvent
- TransactionEvent
Binary file added docs/assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
15 changes: 15 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,28 @@ theme:
- search.highlight
- content.code.annotate
- content.code.copy
logo: assets/logo.png

nav:
- Overview: index.md
- install.md
- usage.md
- api_reference.md

markdown_extensions:
- admonition
- pymdownx.details
- pymdownx.superfences

plugins:
- search
- mkdocstrings:
default_handler: python
handlers:
python:
options:
show_source: false
docstring_style: google
find_stubs_package: true
docstring_options:
ignore_init_summary: false
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ test = [
"coverage[toml] >=7",
"exceptiongroup; python_version<'3.11'",
]
docs = [ "mkdocs", "mkdocs-material" ]
docs = [
"mkdocs",
"mkdocs-material",
"mkdocstrings[python]",
]

[project.urls]
Homepage = "https://github.com/jupyter-server/pycrdt"
Expand Down
1 change: 1 addition & 0 deletions python/pycrdt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from ._doc import Doc as Doc
from ._map import Map as Map
from ._map import MapEvent as MapEvent
from ._pycrdt import SubdocsEvent as SubdocsEvent
from ._pycrdt import Subscription as Subscription
from ._pycrdt import TransactionEvent as TransactionEvent
from ._sync import Decoder as Decoder
Expand Down
175 changes: 170 additions & 5 deletions python/pycrdt/_doc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Any, Callable, Type, TypeVar, cast
from typing import Any, Callable, Iterable, Type, TypeVar, cast

from ._base import BaseDoc, BaseType, base_types
from ._pycrdt import Doc as _Doc
Expand All @@ -12,6 +12,15 @@


class Doc(BaseDoc):
"""
A shared document.
All shared types live within the scope of their corresponding documents.
All updates are generated on a per-document basis.
All operations on shared types happen in a transaction, whose lifetime is also bound to a
document.
"""

def __init__(
self,
init: dict[str, BaseType] = {},
Expand All @@ -21,6 +30,12 @@ def __init__(
Model=None,
allow_multithreading: bool = False,
) -> None:
"""
Args:
init: The initial root types of the document.
client_id: An optional client ID for the document.
allow_multithreading: Whether to allow the document to be used in different threads.
"""
super().__init__(
client_id=client_id, doc=doc, Model=Model, allow_multithreading=allow_multithreading
)
Expand All @@ -31,13 +46,36 @@ def __init__(

@property
def guid(self) -> int:
"""The GUID of the document."""
return self._doc.guid()

@property
def client_id(self) -> int:
"""The document client ID."""
return self._doc.client_id()

def transaction(self, origin: Any = None) -> Transaction:
"""
Creates a new transaction or gets the current one, if any.
If an origin is passed and there is already an ongoing transaction,
the passed origin must be the same as the origin of the current transaction.
This method must be used with a context manager:
```py
with doc.transaction():
...
```
Args:
origin: An optional origin to set on this transaction.
Raises:
RuntimeError: Nested transactions must have same origin as root transaction.
Returns:
A new transaction or the current one.
"""
if self._txn is not None:
if origin is not None:
if origin != self._txn.origin:
Expand All @@ -48,20 +86,71 @@ def transaction(self, origin: Any = None) -> Transaction:
return Transaction(self, origin=origin)

def new_transaction(self, origin: Any = None, timeout: float | None = None) -> NewTransaction:
"""
Creates a new transaction.
Unlike [transaction()][pycrdt.Doc.transaction], this method will not reuse an ongoing
transaction.
If there is already an ongoing transaction, this method will wait (with an optional timeout)
until the current transaction has finished.
There are two ways to do so:
- Use an async context manager:
```py
async with doc.new_transaction():
...
```
In this case you most likely access the document in the same thread, which means that
the [Doc][pycrdt.Doc.__init__] can be created with `allow_multithreading=False`.
- Use a (sync) context manager:
```py
with doc.new_transaction():
...
```
In this case you want to use multithreading, as the ongoing transaction must
run in another thread (otherwise this will deadlock), which means that
the [Doc][pycrdt.Doc.__init__] must have been created with `allow_multithreading=True`.
Args:
origin: An optional origin to set on this transaction.
timeout: An optional timeout (in seconds) to acquire a new transaction.
Raises:
RuntimeError: Already in a transaction.
TimeoutError: Could not acquire transaction.
Returns:
A new transaction.
"""
return NewTransaction(self, origin=origin, timeout=timeout)

def _read_transaction(self, _txn: _Transaction) -> ReadTransaction:
return ReadTransaction(self, _txn)

def get_state(self) -> bytes:
"""
Returns:
The current document state.
"""
return self._doc.get_state()

def get_update(self, state: bytes | None = None) -> bytes:
"""
Args:
state: The optional document state from which to get the update.
Returns:
The update from the given document state (if any), or from the document creation.
"""
if state is None:
state = b"\x00"
return self._doc.get_update(state)

def apply_update(self, update: bytes) -> None:
"""
Args:
update: The update to apply to the document.
"""
if self._Model is not None:
twin_doc = cast(Doc, self._twin_doc)
twin_doc.apply_update(update)
Expand All @@ -74,30 +163,82 @@ def apply_update(self, update: bytes) -> None:
self._doc.apply_update(update)

def __setitem__(self, key: str, value: BaseType) -> None:
"""
Sets a document root type:
```py
doc["text"] = Text("Hello")
```
Args:
key: The name of the root type.
value: The root type.
Raises:
RuntimeError: Key must be of type string.
"""
if not isinstance(key, str):
raise RuntimeError("Key must be of type string")
integrated = value._get_or_insert(key, self)
prelim = value._integrate(self, integrated)
value._init(prelim)

def __getitem__(self, key: str) -> BaseType:
"""
Gets the document root type corresponding to the given key:
```py
text = doc["text"]
```
Args:
key: The key of the root type to get.
Returns:
The document root type.
"""
return self._roots[key]

def __iter__(self):
def __iter__(self) -> Iterable[str]:
"""
Returns:
An iterable over the keys of the document root types.
"""
return iter(self.keys())

def get(self, key: str, *, type: type[T_BaseType]) -> T_BaseType:
"""
Gets the document root type corresponding to the given key.
If it already exists, it will be cast to the given type (if different),
otherwise a new root type is created.
```py
doc.get("text", type=Text)
```
Returns:
The root type corresponding to the given key, cast to the given type.
"""
value = type()
self[key] = value
return value

def keys(self):
def keys(self) -> Iterable[str]:
"""
Returns:
An iterable over the names of the document root types.
"""
return self._roots.keys()

def values(self):
def values(self) -> Iterable[BaseType]:
"""
Returns:
An iterable over the document root types.
"""
return self._roots.values()

def items(self):
def items(self) -> Iterable[tuple[str, BaseType]]:
"""
Returns:
An iterable over the key-value pairs of document root types.
"""
return self._roots.items()

@property
Expand All @@ -114,16 +255,40 @@ def _roots(self) -> dict[str, BaseType]:
}

def observe(self, callback: Callable[[TransactionEvent], None]) -> Subscription:
"""
Subscribes a callback to be called with the document change event.
Args:
callback: The callback to call with the [TransactionEvent][pycrdt.TransactionEvent].
Returns:
The subscription that can be used to [unobserve()][pycrdt.Doc.unobserve].
"""
subscription = self._doc.observe(callback)
self._subscriptions.append(subscription)
return subscription

def observe_subdocs(self, callback: Callable[[SubdocsEvent], None]) -> Subscription:
"""
Subscribes a callback to be called with the document subdoc change event.
Args:
callback: The callback to call with the [SubdocsEvent][pycrdt.SubdocsEvent].
Returns:
The subscription that can be used to [unobserve()][pycrdt.Doc.unobserve].
"""
subscription = self._doc.observe_subdocs(callback)
self._subscriptions.append(subscription)
return subscription

def unobserve(self, subscription: Subscription) -> None:
"""
Unsubscribes to changes using the given subscription.
Args:
subscription: The subscription to unregister.
"""
self._subscriptions.remove(subscription)
subscription.drop()

Expand Down
17 changes: 10 additions & 7 deletions python/pycrdt/_pycrdt.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ class Doc:
Returns a subscription that can be used to unsubscribe."""

class Subscription:
"""Observer subscription"""
"""Observer subscription."""

def drop(self) -> None:
"""Drop the subscription, effectively unobserving."""
"""Drops the subscription, effectively unobserving."""

class Transaction:
"""Document transaction"""
Expand All @@ -68,16 +68,19 @@ class Transaction:
"""The origin of the transaction."""

class TransactionEvent:
"""Event generated by `Doc.observe` method. Emitted during transaction commit
phase."""
"""
Event generated by the [observe][pycrdt.Doc.observe] method,
emitted during the transaction commit phase.
"""

@property
def update(self) -> bytes:
"""The emitted binary update"""
"""The emitted binary update."""

class SubdocsEvent:
"""Event generated by `Doc.observe_subdocs` method. Emitted during transaction commit
phase."""
"""
Event generated by the [observe_subdocs][pycrdt.Doc.observe_subdocs] method,
emitted during the transaction commit phase."""

class TextEvent:
"""Event generated by `Text.observe` method. Emitted during transaction commit
Expand Down

0 comments on commit 9b32184

Please sign in to comment.