From 8808405a3a3461e7ac8909ca28ac473b590a520f Mon Sep 17 00:00:00 2001 From: Camillo Schenone Date: Wed, 22 Nov 2023 12:25:08 +0100 Subject: [PATCH] First steps towards the migration, updated the bindings, and python side wrapper Former-commit-id: fe60dec9d36c28b9f86048a0791349a804c66c8f --- Cargo.toml | 2 +- ...client.cpython-38-darwin.so.REMOVED.git-id | 2 +- src/codemp_client.py | 74 ++--- src/lib.rs | 259 ++++-------------- 4 files changed, 81 insertions(+), 256 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 61fb93d..b6f4a6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ name = "codemp_client" crate-type = ["cdylib"] [dependencies] -codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", tag = "v0.4.4"} +codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", branch = "woot"} pyo3 = { version = "0.19", features = ["extension-module"] } pyo3-asyncio = { version = "0.19", features = ["tokio-runtime"] } tokio = "1.29.1" diff --git a/bindings/codemp_client.cpython-38-darwin.so.REMOVED.git-id b/bindings/codemp_client.cpython-38-darwin.so.REMOVED.git-id index 59ed00c..8891afa 100644 --- a/bindings/codemp_client.cpython-38-darwin.so.REMOVED.git-id +++ b/bindings/codemp_client.cpython-38-darwin.so.REMOVED.git-id @@ -1 +1 @@ -a4493ce329ed791cd4bd17b102efdde3ca4acd8f \ No newline at end of file +293406e9737fd8937bf0b8c3e835778531c58115 \ No newline at end of file diff --git a/src/codemp_client.py b/src/codemp_client.py index 58c412f..3ad7f67 100644 --- a/src/codemp_client.py +++ b/src/codemp_client.py @@ -5,34 +5,40 @@ class CodempClient(): def __init__(self): self.handle = libcodemp.codemp_init() - +## Bindings async def connect(self, server_host): # -> None await self.handle.connect(server_host) - - def disconnect(self): # -> None - self.handle = None + + async def join(self, session): # -> CursorController + return CursorController(await self.handle.join(session)) async def create(self, path, content=None): # -> None - await self.handle.create(path, content) - - # join a workspace - async def join(self, session): # -> CursorController - return CursorController(await self.handle.join(session)) + await self.handle.create(path, content) async def attach(self, path): # -> BufferController - return BufferController(await self.handle.attach(path)) + return BufferController(await self.handle.attach(path)) async def get_cursor(self): # -> CursorController - return CursorController(await self.handle.get_cursor()) + return CursorController(await self.handle.get_cursor()) async def get_buffer(self, path): # -> BufferController - return BufferController(await self.handle.get_buffer()) - - async def disconnect_buffer(self, path): # -> None - await self.handle.disconnect_buffer(path) + return BufferController(await self.handle.get_buffer()) async def leave_workspace(self): # -> None - pass # todo + await self.handle.leave_workspace() + + async def disconnect_buffer(self, path): # -> None + await self.handle.disconnect_buffer(path) + + async def select_buffer(): # -> String + await self.handle.select_buffer() + +## Custom + async def disconnect(self): # -> None + # disconnect all buffers and workspaces first, maybe? + await self.leave_workspace() + # drop the handle, it will require a new instantiation + self.handle = None class CursorController(): def __init__(self, handle): @@ -51,38 +57,12 @@ class CursorController(): # await until new cursor event, then returns return await self.handle.poll() - def drop_callback(self): # -> None - self.handle.drop_callback() - - def callback(self, coro): # -> None - self.handle.callback(coro) - class BufferController(): def __init__(self, handle): self.handle = handle - def get_content(self): # -> String - return self.handle.content() - - def replace(self, txt): # -> None - # replace the whole buffer. - self.handle.replace(txt) - - def insert(self, txt, pos): # -> None - # insert text at buffer position pos - self.handle.insert(txt, pos) - - def delta(self, start, txt, end): # -> None - # delta in the region start..end with txt new content - self.handle.delta(start, txt, end) - - def delete(self, pos, count): # -> None - # delete starting from pos, count chars. - self.handle.delete(pos, count) - - def cancel(self, pos, count): # -> None - # cancel backward `count` elements from pos. - self.handle.cancle(pos, count) + def send(self, start, end, txt): # -> None + self.handle.send(start, end, txt) def try_recv(self): # -> Optional[TextChange] return self.handle.try_recv() @@ -93,12 +73,6 @@ class BufferController(): async def poll(self): # -> ?? return await self.handle.poll() - def drop_callback(self): # -> None - self.handle.drop_callback() - - def callback(self, coro): # -> None - self.handle.callback(coro) - diff --git a/src/lib.rs b/src/lib.rs index ebb4edd..f243fe3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,8 +5,7 @@ use codemp::errors::Error as CodempError; use pyo3::{ prelude::*, - exceptions::{PyConnectionError, PyRuntimeError, PyBaseException}, - types::PyString + exceptions::{PyConnectionError, PyRuntimeError, PyBaseException} }; struct PyCodempError(CodempError); @@ -28,6 +27,9 @@ impl From for PyErr { CodempError::InvalidState { msg } => { PyRuntimeError::new_err(format!("Invalid state: {}", msg)) }, + CodempError::Deadlocked => { + PyRuntimeError::new_err(format!("Deadlock, retry.")) + }, CodempError::Filler { message } => { PyBaseException::new_err(format!("Generic error: {}", message)) } @@ -42,7 +44,6 @@ fn codemp_init<'a>(py: Python<'a>) -> PyResult> { } #[pyclass] -#[derive(Clone)] struct PyClientHandle(Arc); impl From:: for PyClientHandle { @@ -65,6 +66,22 @@ impl PyClientHandle { }) } + // join a workspace + fn join<'a>(&'a self, py: Python<'a>, session: String) -> PyResult<&'a PyAny> { + let rc = self.0.clone(); + + pyo3_asyncio::tokio::future_into_py(py, async move { + let curctrl: PyCursorController = rc.join(session.as_str()) + .await + .map_err(PyCodempError::from)? + .into(); + + Python::with_gil(|py| { + Ok(Py::new(py, curctrl)?) + }) + }) + } + fn create<'a>(&'a self, py: Python<'a>, path: String, content: Option) -> PyResult<&'a PyAny> { let rc = self.0.clone(); @@ -90,22 +107,6 @@ impl PyClientHandle { }) }) } - - // join a workspace - fn join<'a>(&'a self, py: Python<'a>, session: String) -> PyResult<&'a PyAny> { - let rc = self.0.clone(); - - pyo3_asyncio::tokio::future_into_py(py, async move { - let curctrl: PyCursorController = rc.join(session.as_str()) - .await - .map_err(PyCodempError::from)? - .into(); - - Python::with_gil(|py| { - Ok(Py::new(py, curctrl)?) - }) - }) - } fn get_cursor<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { let rc = self.0.clone(); @@ -158,98 +159,47 @@ impl PyClientHandle { Ok(()) }) } -} -#[pyclass] -struct PyCursorController { - handle: Arc, - cb_trigger: Option> -} + fn select_buffer<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { + let rc = self.0.clone(); -impl From::> for PyCursorController { - fn from(value: Arc) -> Self { - PyCursorController { - handle: value, - cb_trigger: None - } + pyo3_asyncio::tokio::future_into_py(py, async move { + let cont = rc.select_buffer() + .await + .map_err(PyCodempError::from)?; + + Ok(cont) + }) } } -fn py_cursor_callback_wrapper(cb: PyObject) - -> Box () + Send + Sync + 'static> -{ - let closure = move |data: CodempCursorEvent| { - let args: PyCursorEvent = data.into(); - Python::with_gil(|py| { let _ = cb.call1(py, (args,)); }); - }; - Box::new(closure) + +/* ########################################################################### */ + +#[pyclass] +struct PyCursorController(Arc); + +impl From::> for PyCursorController { + fn from(value: Arc) -> Self { + PyCursorController(value) + } } #[pymethods] impl PyCursorController { - // fn callback<'a>(&'a self, py: Python<'a>, coro_py: Py, caller_id: Py) -> PyResult<&'a PyAny> { - // let mut rc = self.0.clone(); - // let cb = coro_py.clone(); - // // We want to start polling the ControlHandle and call the callback every time - // // we have something. - - // pyo3_asyncio::tokio::future_into_py(py, async move { - // while let Some(op) = rc.poll().await { - // let start = op.start.unwrap_or(Position { row: 0, col: 0}); - // let end = op.end.unwrap_or(Position { row: 0, col: 0}); - - // let cb_fut = Python::with_gil(|py| -> PyResult<_> { - // let args = (op.user, caller_id.clone(), op.buffer, (start.row, start.col), (end.row, end.col)); - // let coro = cb.call1(py, args)?; - // pyo3_asyncio::tokio::into_future(coro.into_ref(py)) - // })?; - - // cb_fut.await?; - // } - // Ok(()) - // }) - // } - fn drop_callback(&mut self) -> PyResult<()> { - if let Some(channel) = &self.cb_trigger { - channel.send(()) - .map_err(CodempError::from) - .map_err(PyCodempError::from)?; - - self.cb_trigger = None; - } - Ok(()) - } - - fn callback<'a>(&'a mut self, py_cb: Py) -> PyResult<()> { - if let Some(_channel) = &self.cb_trigger { - Err(PyCodempError::from(CodempError::InvalidState { msg: "A callback is already running.".into() }).into()) - } else { - let rt = pyo3_asyncio::tokio::get_runtime(); - - // create a channel to stop the callback task running on the tokio runtime. - // and save the sendent inside the python object, so that we can later call it. - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - self.cb_trigger = Some(tx); - - self.handle.callback(rt, rx, py_cursor_callback_wrapper(py_cb)); - Ok(()) - } - } fn send<'a>(&'a self, path: String, start: (i32, i32), end: (i32, i32)) -> PyResult<()> { - let rc = self.handle.clone(); let pos = CodempCursorPosition { buffer: path, start: Some(start.into()), end: Some(end.into()) }; - rc.send(pos).map_err(PyCodempError::from)?; - Ok(()) + Ok(self.0.send(pos).map_err(PyCodempError::from)?) } fn try_recv(&self, py: Python<'_>) -> PyResult { - match self.handle.try_recv().map_err(PyCodempError::from)? { + match self.0.try_recv().map_err(PyCodempError::from)? { Some(cur_event) => { let evt = PyCursorEvent::from(cur_event); Ok(evt.into_py(py)) @@ -259,7 +209,7 @@ impl PyCursorController { } fn recv<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let rc = self.handle.clone(); + let rc = self.0.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { let cur_event: PyCursorEvent = rc.recv() @@ -273,7 +223,7 @@ impl PyCursorController { } fn poll<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let rc = self.handle.clone(); + let rc = self.0.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { Ok(rc.poll().await.map_err(PyCodempError::from)?) @@ -282,129 +232,30 @@ impl PyCursorController { } #[pyclass] -struct PyBufferController { - handle: Arc, - cb_trigger: Option> -} +struct PyBufferController(Arc); impl From::> for PyBufferController { fn from(value: Arc) -> Self { - PyBufferController{ - handle: value, - cb_trigger: None - } + PyBufferController(value) } } -fn py_buffer_callback_wrapper(cb: PyObject) - -> Box () + Send + Sync + 'static> -{ - let closure = move |data: CodempTextChange| { - let args: PyTextChange = data.into(); - Python::with_gil(|py| { let _ = cb.call1(py, (args,)); }); - }; - Box::new(closure) -} - #[pymethods] impl PyBufferController { - // fn callback<'a>(&'a self, py: Python<'a>, coro_py: Py, caller_id: Py) -> PyResult<&'a PyAny> { - // let mut rc = self.0.clone(); - // let cb = coro_py.clone(); - // // We want to start polling the ControlHandle and call the callback every time - // // we have something. - - // pyo3_asyncio::tokio::future_into_py(py, async move { - // while let Some(edit) = rc.poll().await { - // let start = edit.span.start; - // let end = edit.span.end; - // let text = edit.content; - - // let cb_fut = Python::with_gil(|py| -> PyResult<_> { - // let args = (caller_id.clone(), start, end, text); - // let coro = cb.call1(py, args)?; - // pyo3_asyncio::tokio::into_future(coro.into_ref(py)) - // })?; - - // cb_fut.await?; - // } - // Ok(()) - // }) - // } - - fn drop_callback(&mut self) -> PyResult<()> { - if let Some(channel) = &self.cb_trigger { - channel.send(()) - .map_err(CodempError::from) - .map_err(PyCodempError::from)?; - - self.cb_trigger = None; - } - Ok(()) - } - - fn callback<'a>(&'a mut self, py_cb: Py) -> PyResult<()> { - if let Some(_channel) = &self.cb_trigger { - Err(PyCodempError::from(CodempError::InvalidState { msg: "A callback is already running.".into() }).into()) - } else { - let rt = pyo3_asyncio::tokio::get_runtime(); - - // could this be a oneshot channel? - let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); - self.cb_trigger = Some(tx); - - self.handle.callback(rt, rx, py_buffer_callback_wrapper(py_cb)); - Ok(()) - } - } - - - fn replace(&self, txt: &str) -> PyResult<()> { - if let Some(op) = self.handle.replace(txt) { - self.handle.send(op).map_err(PyCodempError::from)?; - } - Ok(()) - } - - fn delta(&self, start: usize, txt: &str, end: usize) -> PyResult<()> { - if let Some(op) = self.handle.delta(start, txt, end){ - self.handle.send(op).map_err(PyCodempError::from)?; - } - Ok(()) - } - - fn insert(&self, txt: &str, pos: u64) -> PyResult<()> { - let op = self.handle.insert(txt, pos); - self.handle.send(op).map_err(PyCodempError::from)?; - Ok(()) - } - - fn delete(&self, pos: u64, count: u64) -> PyResult<()> { - let op = self.handle.delete(pos, count); - self.handle.send(op).map_err(PyCodempError::from)?; - Ok(()) - } - - fn cancel(&self, pos: u64, count: u64) -> PyResult<()> { - let op = self.handle.cancel(pos, count); - self.handle.send(op).map_err(PyCodempError::from)?; - Ok(()) - } - - fn content(&self, py: Python<'_>) -> PyResult> { - let cont: Py = PyString::new(py, self.handle.content().as_str()).into(); - Ok(cont) - } // TODO: What to do with this send? // does it make sense to implement it at all for the python side?? - // fn send<'a>(&self, py: Python<'a>, skip: usize, text: String, tail: usize) -> PyResult<&'a PyAny>{ - // todo!() - // } + fn send(&self, start: usize, end: usize, txt: String) -> PyResult<()>{ + let op = CodempTextChange { + span: start..end, + content: txt.into() + }; + Ok(self.0.send(op).map_err(PyCodempError::from)?) + } fn try_recv(&self, py: Python<'_>) -> PyResult { - match self.handle.try_recv().map_err(PyCodempError::from)? { + match self.0.try_recv().map_err(PyCodempError::from)? { Some(txt_change) => { let evt = PyTextChange::from(txt_change); Ok(evt.into_py(py)) @@ -414,7 +265,7 @@ impl PyBufferController { } fn recv<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let rc = self.handle.clone(); + let rc = self.0.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { let txt_change: PyTextChange = rc.recv() @@ -428,7 +279,7 @@ impl PyBufferController { } fn poll<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> { - let rc = self.handle.clone(); + let rc = self.0.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { Ok(rc.poll().await.map_err(PyCodempError::from)?)