diff --git a/docs/api_reference.md b/docs/api_reference.md new file mode 100644 index 0000000..6859e0a --- /dev/null +++ b/docs/api_reference.md @@ -0,0 +1,9 @@ +# API reference + +::: pycrdt + options: + members: + - Doc + - Subscription + - SubdocsEvent + - TransactionEvent diff --git a/docs/assets/logo.png b/docs/assets/logo.png new file mode 100644 index 0000000..21ece95 Binary files /dev/null and b/docs/assets/logo.png differ diff --git a/mkdocs.yml b/mkdocs.yml index 5313255..ae6b0fc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 5377431..bf26c74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/python/pycrdt/__init__.py b/python/pycrdt/__init__.py index 94f2bf8..0ec5e08 100644 --- a/python/pycrdt/__init__.py +++ b/python/pycrdt/__init__.py @@ -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 diff --git a/python/pycrdt/_doc.py b/python/pycrdt/_doc.py index d7dfa9f..eb0be01 100644 --- a/python/pycrdt/_doc.py +++ b/python/pycrdt/_doc.py @@ -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 @@ -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] = {}, @@ -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 ) @@ -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: @@ -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) @@ -74,6 +163,19 @@ 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) @@ -81,23 +183,62 @@ def __setitem__(self, key: str, value: BaseType) -> None: 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 @@ -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() diff --git a/python/pycrdt/_pycrdt.pyi b/python/pycrdt/_pycrdt.pyi index 80ef5f4..143a4c3 100644 --- a/python/pycrdt/_pycrdt.pyi +++ b/python/pycrdt/_pycrdt.pyi @@ -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""" @@ -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