First steps towards the migration, updated the bindings, and python side wrapper

Former-commit-id: fe60dec9d36c28b9f86048a0791349a804c66c8f
This commit is contained in:
Camillo Schenone 2023-11-22 12:25:08 +01:00
parent 9a964a099b
commit 8808405a3a
4 changed files with 81 additions and 256 deletions

View file

@ -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"

View file

@ -1 +1 @@
a4493ce329ed791cd4bd17b102efdde3ca4acd8f
293406e9737fd8937bf0b8c3e835778531c58115

View file

@ -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)

View file

@ -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<PyCodempError> 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<Py<PyClientHandle>> {
}
#[pyclass]
#[derive(Clone)]
struct PyClientHandle(Arc<CodempInstance>);
impl From::<CodempInstance> 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<String>) -> 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<CodempCursorController>,
cb_trigger: Option<tokio::sync::mpsc::UnboundedSender<()>>
}
fn select_buffer<'a>(&'a self, py: Python<'a>) -> PyResult<&'a PyAny> {
let rc = self.0.clone();
impl From::<Arc<CodempCursorController>> for PyCursorController {
fn from(value: Arc<CodempCursorController>) -> 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<dyn FnMut(CodempCursorEvent) -> () + 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<CodempCursorController>);
impl From::<Arc<CodempCursorController>> for PyCursorController {
fn from(value: Arc<CodempCursorController>) -> Self {
PyCursorController(value)
}
}
#[pymethods]
impl PyCursorController {
// fn callback<'a>(&'a self, py: Python<'a>, coro_py: Py<PyAny>, caller_id: Py<PyString>) -> 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<PyAny>) -> 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<PyObject> {
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<CodempBufferController>,
cb_trigger: Option<tokio::sync::mpsc::UnboundedSender<()>>
}
struct PyBufferController(Arc<CodempBufferController>);
impl From::<Arc<CodempBufferController>> for PyBufferController {
fn from(value: Arc<CodempBufferController>) -> Self {
PyBufferController{
handle: value,
cb_trigger: None
}
PyBufferController(value)
}
}
fn py_buffer_callback_wrapper(cb: PyObject)
-> Box<dyn FnMut(CodempTextChange) -> () + 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<PyAny>, caller_id: Py<PyString>) -> 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<PyAny>) -> 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<Py<PyString>> {
let cont: Py<PyString> = 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<PyObject> {
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)?)