diff --git a/.gitignore b/.gitignore index a22c944..8a94fdd 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,9 @@ Cargo.lock .vscode/ *.sublime-* +.venv +wheels/ + # js node_modules/ package-lock.json diff --git a/dist/py/.python-version b/dist/py/.python-version new file mode 100644 index 0000000..cc1923a --- /dev/null +++ b/dist/py/.python-version @@ -0,0 +1 @@ +3.8 diff --git a/dist/py/build.sh b/dist/py/build.sh new file mode 100755 index 0000000..3689d74 --- /dev/null +++ b/dist/py/build.sh @@ -0,0 +1,7 @@ +ROOT_DIR="$(pwd)" +WHEEL_DIR="$ROOT_DIR/wheels" + +PYO3_PYTHON="$(pyenv which python)" +TARGET_EXT="$($PYO3_PYTHON -c 'import sysconfig; print(sysconfig.get_config_var("EXT_SUFFIX"))')" + +maturin build -i "$PYO3_PYTHON" --out "$WHEEL_DIR" diff --git a/dist/py/codemp.pyi b/dist/py/codemp.pyi new file mode 100644 index 0000000..8fb07b9 --- /dev/null +++ b/dist/py/codemp.pyi @@ -0,0 +1,65 @@ +from typing import Tuple + +def init_logger(debug: bool | None) -> PyLogger: ... + +def codemp_init() -> Client: ... + +class PyLogger: + async def message(self) -> str | None: ... + + +class CodempTextChange: + start_incl: int + end_excl: int + content: str + + def is_deletion(self) -> bool: ... + def is_addition(self) -> bool: ... + def is_empty(self) -> bool: ... + def apply(self, txt: str) -> str: ... + def from_diff(self, before: str, after: str) -> CodempTextChange: ... + def index_to_rowcol(self, txt: str, index: int) -> Tuple[int, int]: ... + + +class CodempBufferController: + def content(self) -> str: ... + def send(self, start: int, end: int, txt: str) -> None: ... + async def try_recv(self) -> CodempTextChange | None: ... + async def recv(self) -> CodempTextChange: ... + async def poll(self) -> None: ... + + +class CodempCursor: + start: Tuple[int, int] + end: Tuple[int, int] + buffer: str + user: str # can be an empty string + + +class CodempCursorController: + def send(self, path: str, start: Tuple[int, int], end: Tuple[int, int]) -> None: ... + def try_recv(self) -> CodempCursor | None: ... + async def recv(self) -> CodempCursor: ... + async def poll(self) -> None: ... + + +class CodempWorkspace: + async def create(self, path: str) -> None: ... + async def attach(self, path: str) -> CodempBufferController: ... + async def fetch_buffers(self) -> None: ... + async def fetch_users(self) -> None: ... + async def list_buffer_users(self, path: str) -> list[str]: ... + async def delete(self, path: str) -> None: ... + def id(self) -> str: ... + def cursor(self) -> CodempCursorController: ... + def buffer_by_name(self, path: str) -> CodempBufferController: ... + def filetree(self) -> list[str]: ... + + +class Client: + def __init__(self) -> None: ... + async def connect(self, host: str) -> None: ... + async def login(self, user: str, password: str, workspace: str | None) -> None: ... + async def join_workspace(self, workspace: str) -> CodempWorkspace: ... + async def get_workspace(self, id: str) -> CodempWorkspace: ... + async def user_id(self) -> str: ... diff --git a/dist/py/pyproject.toml b/dist/py/pyproject.toml new file mode 100644 index 0000000..0c1a541 --- /dev/null +++ b/dist/py/pyproject.toml @@ -0,0 +1,30 @@ +[project] +name = "codemp" +version = "0.6.2" +description = "Cooperative multi-player coding" +requires-python = ">=3.8" +license = {file = "../../LICENSE"} +keywords = ["codemp", "ffi", "rust", "python"] +authors = [ + {email = "cschen@codemp.dev"} +] +maintainers = [ + {name = "Camillo Schenone", email = "cschen@codemp.dev"} +] +classifiers = [ + "Programming Language :: Python" +] + +dependencies = [] + +[project.urls] +repository = "github.com/hexedtech/codemp.git" + +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[tool.maturin] +features = ["python"] +manifest-path = "../../Cargo.toml" + diff --git a/src/api/cursor.rs b/src/api/cursor.rs index acffcd9..a809830 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -4,7 +4,6 @@ //! information about their identity use codemp_proto as proto; -// use pyo3::prelude::*; use uuid::Uuid; /// user cursor position in a buffer diff --git a/src/ffi/python.rs b/src/ffi/python.rs index a7563e4..f981b37 100644 --- a/src/ffi/python.rs +++ b/src/ffi/python.rs @@ -1,4 +1,4 @@ -use pyo3::types::PyList; +use pyo3::types::{PyList, PyTuple}; use std::{format, sync::Arc}; use tokio::sync::{mpsc, Mutex, RwLock}; use tracing; @@ -73,8 +73,8 @@ impl PyLogger { // 5. Create a new buffer/attach to an existing one #[pyfunction] -fn codemp_init<'a>(py: Python<'a>) -> PyResult> { - Ok(Py::new(py, PyClient::default())?) +fn codemp_init<'a>(py: Python<'a>) -> PyResult> { + Ok(Py::new(py, Client::default())?) } #[pyfunction] @@ -108,22 +108,22 @@ fn init_logger(py: Python<'_>, debug: Option) -> PyResult> { } #[pyclass] -struct PyClient(Arc>>); +struct Client(Arc>>); -impl Default for PyClient { +impl Default for Client { fn default() -> Self { - PyClient(Arc::new(RwLock::new(None))) + Client(Arc::new(RwLock::new(None))) } } -impl From for PyClient { +impl From for Client { fn from(value: CodempClient) -> Self { - PyClient(RwLock::new(Some(value)).into()) + Client(RwLock::new(Some(value)).into()) } } #[pymethods] -impl PyClient { +impl Client { fn connect<'a>(&'a self, py: Python<'a>, dest: String) -> PyResult<&'a PyAny> { let cli = self.0.clone(); @@ -217,6 +217,7 @@ impl PyClient { #[pymethods] impl CodempWorkspace { // join a workspace + #[pyo3(name = "create")] fn pycreate<'a>(&'a self, py: Python<'a>, path: String) -> PyResult<&'a PyAny> { let ws = self.clone(); @@ -225,7 +226,7 @@ impl CodempWorkspace { Ok(()) }) } - + #[pyo3(name = "attach")] fn pyattach<'a>(&'a self, py: Python<'a>, path: String) -> PyResult<&'a PyAny> { let ws = self.clone(); @@ -235,6 +236,7 @@ impl CodempWorkspace { }) } + #[pyo3(name = "fetch_buffers")] fn pyfetch_buffers<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { let ws = self.clone(); @@ -244,6 +246,7 @@ impl CodempWorkspace { }) } + #[pyo3(name = "fetch_users")] fn pyfetch_users<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { let ws = self.clone(); @@ -253,6 +256,7 @@ impl CodempWorkspace { }) } + #[pyo3(name = "list_buffer_users")] fn pylist_buffer_users<'a>(&'a self, py: Python<'a>, path: String) -> PyResult<&'a PyAny> { let ws = self.clone(); @@ -268,6 +272,7 @@ impl CodempWorkspace { }) } + #[pyo3(name = "delete")] fn pydelete<'a>(&'a self, py: Python<'a>, path: String) -> PyResult<&'a PyAny> { let ws = self.clone(); @@ -277,14 +282,17 @@ impl CodempWorkspace { }) } + #[pyo3(name = "id")] fn pyid(&self, py: Python<'_>) -> Py { PyString::new(py, self.id().as_str()).into() } + #[pyo3(name = "cursor")] fn pycursor(&self, py: Python<'_>) -> PyResult> { Ok(Py::new(py, CodempCursorController::from(self.cursor()))?) } + #[pyo3(name = "buffer_by_name")] fn pybuffer_by_name( &self, py: Python<'_>, @@ -297,6 +305,7 @@ impl CodempWorkspace { Ok(Some(Py::new(py, CodempBufferController::from(bufctl))?)) } + #[pyo3(name = "filetree")] fn pyfiletree(&self, py: Python<'_>) -> Py { PyList::new(py, self.filetree()).into_py(py) } @@ -306,6 +315,7 @@ impl CodempWorkspace { #[pymethods] impl CodempCursorController { + #[pyo3(name = "send")] fn pysend<'a>(&'a self, path: String, start: (i32, i32), end: (i32, i32)) -> PyResult<()> { let pos = CodempCursor { start: start.into(), @@ -317,6 +327,7 @@ impl CodempCursorController { Ok(self.send(pos)?) } + #[pyo3(name = "try_recv")] fn pytry_recv(&self, py: Python<'_>) -> PyResult { match self.try_recv()? { Some(cur_event) => { @@ -327,6 +338,7 @@ impl CodempCursorController { } } + #[pyo3(name = "recv")] fn pyrecv<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { let rc = self.clone(); @@ -336,6 +348,7 @@ impl CodempCursorController { }) } + #[pyo3(name = "poll")] fn pypoll<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { let rc = self.clone(); @@ -343,12 +356,40 @@ impl CodempCursorController { } } +#[pymethods] +impl CodempCursor { + #[getter(start)] + fn pystart(&self, py: Python<'_>) -> Py { + self.start.into_py(py) + } + + #[getter(end)] + fn pyend(&self, py: Python<'_>) -> Py { + self.end.into_py(py) + } + + #[getter(buffer)] + fn pybuffer(&self, py: Python<'_>) -> Py { + PyString::new(py, self.buffer.as_str()).into() + } + + #[getter(user)] + fn pyuser(&self, py: Python<'_>) -> Py { + match self.user { + Some(user) => PyString::new(py, user.to_string().as_str()).into(), + None => "".into_py(py), + } + } +} + #[pymethods] impl CodempBufferController { + #[pyo3(name = "content")] fn pycontent<'a>(&self, py: Python<'a>) -> &'a PyString { PyString::new(py, self.content().as_str()) } + #[pyo3(name = "send")] fn pysend(&self, start: usize, end: usize, txt: String) -> PyResult<()> { let op = CodempTextChange { span: start..end, @@ -357,6 +398,7 @@ impl CodempBufferController { Ok(self.send(op)?) } + #[pyo3(name = "try_recv")] fn pytry_recv(&self, py: Python<'_>) -> PyResult { match self.try_recv()? { Some(txt_change) => { @@ -367,6 +409,7 @@ impl CodempBufferController { } } + #[pyo3(name = "recv")] fn pyrecv<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { let rc = self.clone(); @@ -376,6 +419,7 @@ impl CodempBufferController { }) } + #[pyo3(name = "poll")] fn pypoll<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { let rc = self.clone(); @@ -386,42 +430,51 @@ impl CodempBufferController { #[pymethods] impl CodempTextChange { #[getter] + #[pyo3(name = "start_incl")] fn pystart_incl(&self) -> PyResult { Ok(self.span.start) } #[getter] + #[pyo3(name = "end_excl")] fn pyend_excl(&self) -> PyResult { Ok(self.span.end) } #[getter] + #[pyo3(name = "content")] fn pycontent(&self) -> PyResult { Ok(self.content.clone()) } + #[pyo3(name = "is_deletion")] fn pyis_deletion(&self) -> bool { self.is_deletion() } + #[pyo3(name = "is_addition")] fn pyis_addition(&self) -> bool { self.is_addition() } + #[pyo3(name = "is_empty")] fn pyis_empty(&self) -> bool { self.is_empty() } + #[pyo3(name = "apply")] fn pyapply(&self, txt: &str) -> String { self.apply(txt) } #[classmethod] + #[pyo3(name = "from_diff")] fn pyfrom_diff(_cls: &PyType, before: &str, after: &str) -> CodempTextChange { CodempTextChange::from_diff(before, after) } #[classmethod] + #[pyo3(name = "index_to_rowcol")] fn pyindex_to_rowcol(_cls: &PyType, txt: &str, index: usize) -> (i32, i32) { CodempTextChange::index_to_rowcol(txt, index).into() } @@ -432,13 +485,12 @@ impl CodempTextChange { fn codemp(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(codemp_init, m)?)?; m.add_function(wrap_pyfunction!(init_logger, m)?)?; - m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; m.add_class::()?; - // m.add_class::()?; m.add_class::()?; m.add_class::()?;