From 9b32184d2bd07aa2e75f1a2bb1a9387507afbd38 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 27 Sep 2024 16:49:43 +0200 Subject: [PATCH] Add API reference --- docs/api_reference.md | 9 ++ docs/assets/logo.png | Bin 0 -> 6396 bytes mkdocs.yml | 15 ++++ pyproject.toml | 6 +- python/pycrdt/__init__.py | 1 + python/pycrdt/_doc.py | 175 ++++++++++++++++++++++++++++++++++++-- python/pycrdt/_pycrdt.pyi | 17 ++-- 7 files changed, 210 insertions(+), 13 deletions(-) create mode 100644 docs/api_reference.md create mode 100644 docs/assets/logo.png 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 0000000000000000000000000000000000000000..21ece957792db1f7a0c1106cab971b099fa92bae GIT binary patch literal 6396 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 KMNUMnLSTY6dIF;W literal 0 HcmV?d00001 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