diff --git a/src/lib.rs b/src/lib.rs index 509819c..897f0a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,8 @@ pub mod dependency_map; use dependency_map::*; pub mod operation; use operation::*; +pub mod operations; +use operations::*; mod task; use task::{Annotation, Status, Tag, Task, TaskData}; @@ -23,6 +25,7 @@ fn taskchampion(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/src/operation.rs b/src/operation.rs index ea08401..42cd060 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -1,5 +1,5 @@ use chrono::DateTime; -use pyo3::prelude::*; +use pyo3::{exceptions::PyAttributeError, prelude::*}; use std::collections::HashMap; use taskchampion::{Operation as TCOperation, Uuid}; @@ -9,6 +9,11 @@ use taskchampion::{Operation as TCOperation, Uuid}; pub struct Operation(pub(crate) TCOperation); #[pymethods] +/// An Operation defines a single change to the task database, as stored locally in the replica. +/// +/// This is an enum in Rust, represented here with four static constructors for the variants, +/// four `is_..` methods for determining the type, and getters for each variant field. The +/// getters raise `AttributeError` for variants that do not have the given property. impl Operation { #[allow(non_snake_case)] #[staticmethod] @@ -51,9 +56,110 @@ impl Operation { pub fn UndoPoint() -> Operation { Operation(TCOperation::UndoPoint) } + + pub fn __repr__(&self) -> String { + format!("{:?}", self.0) + } + + pub fn is_create(&self) -> bool { + matches!(self.0, TCOperation::Create { .. }) + } + + pub fn is_delete(&self) -> bool { + matches!(self.0, TCOperation::Delete { .. }) + } + + pub fn is_update(&self) -> bool { + matches!(self.0, TCOperation::Update { .. }) + } + pub fn is_undo_point(&self) -> bool { - self.0.is_undo_point() + matches!(self.0, TCOperation::UndoPoint) + } + + #[getter(uuid)] + pub fn get_uuid(&self) -> PyResult { + use TCOperation::*; + match &self.0 { + Create { uuid } => Ok(uuid.to_string()), + Delete { uuid, .. } => Ok(uuid.to_string()), + Update { uuid, .. } => Ok(uuid.to_string()), + _ => Err(PyAttributeError::new_err( + "Variant does not have attribute 'uuid'", + )), + } + } + + #[getter(old_task)] + pub fn get_old_task(&self) -> PyResult> { + use TCOperation::*; + match &self.0 { + Delete { old_task, .. } => Ok(old_task.clone()), + _ => Err(PyAttributeError::new_err( + "Variant does not have attribute 'old_task'", + )), + } + } + + #[getter(property)] + pub fn get_property(&self) -> PyResult { + use TCOperation::*; + match &self.0 { + Update { property, .. } => Ok(property.clone()), + _ => Err(PyAttributeError::new_err( + "Variant does not have attribute 'property'", + )), + } + } + + #[getter(timestamp)] + pub fn get_timestamp(&self) -> PyResult { + use TCOperation::*; + match &self.0 { + Update { timestamp, .. } => Ok(timestamp.to_string()), + _ => Err(PyAttributeError::new_err( + "Variant does not have attribute 'timestamp'", + )), + } + } + + #[getter(old_value)] + pub fn get_old_value(&self) -> PyResult> { + use TCOperation::*; + match &self.0 { + Update { old_value, .. } => Ok(old_value.clone()), + _ => Err(PyAttributeError::new_err( + "Variant does not have attribute 'old_value'", + )), + } + } + + #[getter(value)] + pub fn get_value(&self) -> PyResult> { + use TCOperation::*; + match &self.0 { + Update { value, .. } => Ok(value.clone()), + _ => Err(PyAttributeError::new_err( + "Variant does not have attribute 'value'", + )), + } + } +} + +impl AsRef for Operation { + fn as_ref(&self) -> &TCOperation { + &self.0 } } -pub type Operations = Vec; +impl AsMut for Operation { + fn as_mut(&mut self) -> &mut TCOperation { + &mut self.0 + } +} + +impl From for TCOperation { + fn from(val: Operation) -> Self { + val.0 + } +} diff --git a/src/operations.rs b/src/operations.rs new file mode 100644 index 0000000..729ea90 --- /dev/null +++ b/src/operations.rs @@ -0,0 +1,58 @@ +use crate::Operation; +use pyo3::{exceptions::PyIndexError, prelude::*}; +use taskchampion::Operations as TCOperations; + +#[pyclass(sequence)] +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct Operations(TCOperations); + +#[pymethods] +impl Operations { + #[new] + pub fn new() -> Operations { + Operations(TCOperations::new()) + } + + pub fn append(&mut self, op: Operation) { + self.0.push(op.into()); + } + + pub fn __repr__(&self) -> String { + format!("{:?}", self) + } + + pub fn __len__(&self) -> usize { + self.0.len() + } + + pub fn __getitem__(&self, i: usize) -> PyResult { + if i >= self.0.len() { + return Err(PyIndexError::new_err("Invalid operation index")); + } + Ok(Operation(self.0[i].clone())) + } +} + +impl AsRef for Operations { + fn as_ref(&self) -> &TCOperations { + &self.0 + } +} + +impl AsMut for Operations { + fn as_mut(&mut self) -> &mut TCOperations { + &mut self.0 + } +} + +impl From for TCOperations { + fn from(val: Operations) -> Self { + val.0 + } +} + +impl From for Operations { + fn from(val: TCOperations) -> Self { + Operations(val) + } +} diff --git a/src/replica.rs b/src/replica.rs index ce1e64d..4d38ca5 100644 --- a/src/replica.rs +++ b/src/replica.rs @@ -2,11 +2,9 @@ use std::collections::HashMap; use std::rc::Rc; use crate::task::TaskData; -use crate::{DependencyMap, Operation, Task, WorkingSet}; +use crate::{DependencyMap, Operations, Task, WorkingSet}; use pyo3::prelude::*; -use taskchampion::{ - Operations as TCOperations, Replica as TCReplica, ServerConfig, StorageConfig, Uuid, -}; +use taskchampion::{Replica as TCReplica, ServerConfig, StorageConfig, Uuid}; #[pyclass] /// A replica represents an instance of a user's task data, providing an easy interface @@ -43,13 +41,12 @@ impl Replica { /// Create a new task /// The task must not already exist. - pub fn create_task(&mut self, uuid: String) -> anyhow::Result<(Task, Vec)> { - let mut ops = TCOperations::new(); + pub fn create_task(&mut self, uuid: String, ops: &mut Operations) -> anyhow::Result { let task = self .0 - .create_task(Uuid::parse_str(&uuid)?, &mut ops) + .create_task(Uuid::parse_str(&uuid)?, ops.as_mut()) .map(Task)?; - Ok((task, ops.iter().map(|op| Operation(op.clone())).collect())) + Ok(task) } /// Get a list of all tasks in the replica. @@ -121,6 +118,10 @@ impl Replica { Ok(self.0.sync(&mut server, avoid_snapshots)?) } + pub fn commit_operations(&mut self, ops: Operations) -> anyhow::Result<()> { + Ok(self.0.commit_operations(ops.into())?) + } + /// Sync with a server created from `ServerConfig::Remote`. fn sync_to_remote( &mut self, @@ -156,13 +157,10 @@ impl Replica { Ok(self.0.sync(&mut server, avoid_snapshots)?) } - pub fn commit_operations(&mut self, operations: Vec) -> anyhow::Result<()> { - let ops = operations.iter().map(|op| op.0.clone()).collect(); - Ok(self.0.commit_operations(ops)?) - } pub fn rebuild_working_set(&mut self, renumber: bool) -> anyhow::Result<()> { Ok(self.0.rebuild_working_set(renumber)?) } + pub fn num_local_operations(&mut self) -> anyhow::Result { Ok(self.0.num_local_operations()?) } @@ -171,20 +169,12 @@ impl Replica { Ok(self.0.num_local_operations()?) } - pub fn get_undo_operations(&mut self) -> anyhow::Result> { - Ok(self - .0 - .get_undo_operations() - .map(|ops| ops.iter().map(|op| Operation(op.clone())).collect())?) + pub fn get_undo_operations(&mut self) -> anyhow::Result { + Ok(self.0.get_undo_operations()?.into()) } - pub fn commit_reversed_operations( - &mut self, - operations: Vec, - ) -> anyhow::Result { - let ops = operations.iter().map(|op| op.0.clone()).collect(); - - Ok(self.0.commit_reversed_operations(ops)?) + pub fn commit_reversed_operations(&mut self, operations: Operations) -> anyhow::Result { + Ok(self.0.commit_reversed_operations(operations.into())?) } pub fn expire_tasks(&mut self) -> anyhow::Result<()> { diff --git a/src/task/data.rs b/src/task/data.rs index b8e2381..de0d552 100644 --- a/src/task/data.rs +++ b/src/task/data.rs @@ -1,6 +1,6 @@ -use crate::Operation; -use pyo3::prelude::*; -use taskchampion::{Operation as TCOperation, TaskData as TCTaskData, Uuid}; +use crate::Operations; +use pyo3::{exceptions::PyValueError, prelude::*}; +use taskchampion::{TaskData as TCTaskData, Uuid}; #[pyclass] pub struct TaskData(pub(crate) TCTaskData); @@ -8,15 +8,12 @@ pub struct TaskData(pub(crate) TCTaskData); #[pymethods] impl TaskData { #[staticmethod] - pub fn create(uuid: String) -> (Self, Operation) { - let u = Uuid::parse_str(&uuid).expect("invalid UUID"); - - let mut ops: Vec = vec![TCOperation::Create { uuid: u }]; - - let td = TaskData(TCTaskData::create(u, &mut ops)); - (td, Operation(ops.first().expect("").clone())) + pub fn create(uuid: String, ops: &mut Operations) -> PyResult { + let u = Uuid::parse_str(&uuid).map_err(|_| PyValueError::new_err("Invalid UUID"))?; + Ok(TaskData(TCTaskData::create(u, ops.as_mut()))) } + #[getter(uuid)] pub fn get_uuid(&self) -> String { self.0.get_uuid().into() } @@ -29,18 +26,12 @@ impl TaskData { self.0.has(value) } - #[pyo3(signature=(property, value=None))] - pub fn update(&mut self, property: String, value: Option) -> Operation { - let mut ops: Vec = Vec::new(); - - self.0.update(property, value, &mut ops); - ops.first().map(|op| Operation(op.clone())).expect("") + #[pyo3(signature=(property, value, ops))] + pub fn update(&mut self, property: String, value: Option, ops: &mut Operations) { + self.0.update(property, value, ops.as_mut()); } - pub fn delete(&mut self) -> Operation { - let mut ops: Vec = Vec::new(); - self.0.delete(&mut ops); - - ops.first().map(|op| Operation(op.clone())).expect("") + pub fn delete(&mut self, ops: &mut Operations) { + self.0.delete(ops.as_mut()); } } diff --git a/src/task/task.rs b/src/task/task.rs index fed54b2..dcba09b 100644 --- a/src/task/task.rs +++ b/src/task/task.rs @@ -1,14 +1,22 @@ use crate::task::{Annotation, Status, Tag, TaskData}; -use crate::Operation; +use crate::Operations; use chrono::{DateTime, Utc}; use pyo3::prelude::*; -use taskchampion::{Operation as TCOperation, Task as TCTask, Uuid}; +use taskchampion::{Task as TCTask, Uuid}; + // TODO: actually create a front-facing user class, instead of this data blob #[pyclass] pub struct Task(pub(crate) TCTask); unsafe impl Send for Task {} +impl Task { + fn to_datetime(s: Option) -> anyhow::Result>> { + s.map(|time| Ok(DateTime::parse_from_rfc3339(&time)?.with_timezone(&chrono::Utc))) + .transpose() + } +} + #[pymethods] impl Task { #[allow(clippy::wrong_self_convention)] @@ -134,6 +142,7 @@ impl Task { pub fn get_udas(&self) -> Vec<((&str, &str), &str)> { self.0.get_udas().collect() } + /// Get the task modified time /// /// Returns: @@ -151,6 +160,7 @@ impl Task { pub fn get_due(&self) -> Option> { self.0.get_due() } + /// Get a list of tasks dependencies /// /// Returns: @@ -161,6 +171,7 @@ impl Task { .map(|uuid| uuid.to_string()) .collect() } + /// Get the task's property value /// /// Returns: @@ -170,157 +181,94 @@ impl Task { self.0.get_value(property) } - pub fn set_status(&mut self, status: Status) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.set_status(status.into(), &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn set_status(&mut self, status: Status, ops: &mut Operations) -> anyhow::Result<()> { + Ok(self.0.set_status(status.into(), ops.as_mut())?) } - pub fn set_description(&mut self, description: String) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.set_description(description, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn set_description( + &mut self, + description: String, + ops: &mut Operations, + ) -> anyhow::Result<()> { + Ok(self.0.set_description(description, ops.as_mut())?) } - pub fn set_priority(&mut self, priority: String) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.set_priority(priority, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn set_priority(&mut self, priority: String, ops: &mut Operations) -> anyhow::Result<()> { + Ok(self.0.set_priority(priority, ops.as_mut())?) } - #[pyo3(signature=(entry=None))] - pub fn set_entry(&mut self, entry: Option) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - let timestamp = entry.map(|time| { - DateTime::parse_from_rfc3339(&time) - .unwrap() - .with_timezone(&chrono::Utc) - }); - - self.0.set_entry(timestamp, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + #[pyo3(signature=(entry, ops))] + pub fn set_entry(&mut self, entry: Option, ops: &mut Operations) -> anyhow::Result<()> { + let timestamp = Self::to_datetime(entry)?; + Ok(self.0.set_entry(timestamp, ops.as_mut())?) } - #[pyo3(signature=(wait=None))] - pub fn set_wait(&mut self, wait: Option) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - let timestamp = wait.map(|time| { - DateTime::parse_from_rfc3339(&time) - .unwrap() - .with_timezone(&chrono::Utc) - }); - - self.0.set_wait(timestamp, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + #[pyo3(signature=(wait, ops))] + pub fn set_wait(&mut self, wait: Option, ops: &mut Operations) -> anyhow::Result<()> { + let timestamp = Self::to_datetime(wait)?; + Ok(self.0.set_wait(timestamp, ops.as_mut())?) } - #[pyo3(signature=(modified=None))] - pub fn set_modified(&mut self, modified: Option) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - let timestamp = modified.map(|time| { - DateTime::parse_from_rfc3339(&time) - .unwrap() - .with_timezone(&chrono::Utc) - }); - - self.0.set_wait(timestamp, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + #[pyo3(signature=(modified, ops))] + pub fn set_modified(&mut self, modified: String, ops: &mut Operations) -> anyhow::Result<()> { + let timestamp = DateTime::parse_from_rfc3339(&modified)?.with_timezone(&chrono::Utc); + Ok(self.0.set_modified(timestamp, ops.as_mut())?) } - #[pyo3(signature=(property, value=None))] + #[pyo3(signature=(property, value, ops))] pub fn set_value( &mut self, property: String, value: Option, - ) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - self.0.set_value(property, value, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + ops: &mut Operations, + ) -> anyhow::Result<()> { + Ok(self.0.set_value(property, value, ops.as_mut())?) } - pub fn start(&mut self) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.start(&mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn start(&mut self, ops: &mut Operations) -> anyhow::Result<()> { + Ok(self.0.start(ops.as_mut())?) } - pub fn stop(&mut self) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.stop(&mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn stop(&mut self, ops: &mut Operations) -> anyhow::Result<()> { + Ok(self.0.stop(ops.as_mut())?) } - pub fn done(&mut self) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.done(&mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn done(&mut self, ops: &mut Operations) -> anyhow::Result<()> { + Ok(self.0.done(ops.as_mut())?) } - pub fn add_tag(&mut self, tag: &Tag) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.add_tag(tag.as_ref(), &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn add_tag(&mut self, tag: &Tag, ops: &mut Operations) -> anyhow::Result<()> { + Ok(self.0.add_tag(tag.as_ref(), ops.as_mut())?) } - pub fn remove_tag(&mut self, tag: &Tag) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.remove_tag(tag.as_ref(), &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn remove_tag(&mut self, tag: &Tag, ops: &mut Operations) -> anyhow::Result<()> { + Ok(self.0.remove_tag(tag.as_ref(), ops.as_mut())?) } - pub fn add_annotation(&mut self, ann: &Annotation) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); + pub fn add_annotation(&mut self, ann: &Annotation, ops: &mut Operations) -> anyhow::Result<()> { + // Create an owned annotation let mut annotation = Annotation::new(); - annotation.set_entry(ann.entry()); annotation.set_description(ann.description()); - self.0.add_annotation(annotation.0, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + Ok(self.0.add_annotation(annotation.0, ops.as_mut())?) } pub fn remove_annotation( &mut self, timestamp: DateTime, - ) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.remove_annotation(timestamp, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + ops: &mut Operations, + ) -> anyhow::Result<()> { + Ok(self.0.remove_annotation(timestamp, ops.as_mut())?) } - #[pyo3(signature=(due=None))] - pub fn set_due(&mut self, due: Option>) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.set_due(due, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + #[pyo3(signature=(due, ops))] + pub fn set_due( + &mut self, + due: Option>, + ops: &mut Operations, + ) -> anyhow::Result<()> { + Ok(self.0.set_due(due, ops.as_mut())?) } pub fn set_uda( @@ -328,53 +276,40 @@ impl Task { namespace: String, key: String, value: String, - ) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.set_uda(namespace, key, value, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + ops: &mut Operations, + ) -> anyhow::Result<()> { + Ok(self.0.set_uda(namespace, key, value, ops.as_mut())?) } - pub fn remove_uda(&mut self, namespace: String, key: String) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.remove_uda(namespace, key, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn remove_uda( + &mut self, + namespace: String, + key: String, + ops: &mut Operations, + ) -> anyhow::Result<()> { + Ok(self.0.remove_uda(namespace, key, ops.as_mut())?) } - pub fn set_legacy_uda(&mut self, key: String, value: String) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.set_legacy_uda(key, value, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn set_legacy_uda( + &mut self, + key: String, + value: String, + ops: &mut Operations, + ) -> anyhow::Result<()> { + Ok(self.0.set_legacy_uda(key, value, ops.as_mut())?) } - pub fn remove_legacy_uda(&mut self, key: String) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - - self.0.remove_legacy_uda(key, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn remove_legacy_uda(&mut self, key: String, ops: &mut Operations) -> anyhow::Result<()> { + Ok(self.0.remove_legacy_uda(key, ops.as_mut())?) } - pub fn add_dependency(&mut self, dep: String) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - let dep_uuid = Uuid::parse_str(&dep).expect("couldn't parse UUID"); - - self.0.add_dependency(dep_uuid, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn add_dependency(&mut self, dep: String, ops: &mut Operations) -> anyhow::Result<()> { + let dep_uuid = Uuid::parse_str(&dep)?; + Ok(self.0.add_dependency(dep_uuid, ops.as_mut())?) } - pub fn remove_dependency(&mut self, dep: String) -> anyhow::Result> { - let mut ops: Vec = Vec::new(); - let dep_uuid = Uuid::parse_str(&dep).expect("couldn't parse UUID"); - - self.0.remove_dependency(dep_uuid, &mut ops).expect(""); - - Ok(ops.iter().map(|op| Operation(op.clone())).collect()) + pub fn remove_dependency(&mut self, dep: String, ops: &mut Operations) -> anyhow::Result<()> { + let dep_uuid = Uuid::parse_str(&dep)?; + Ok(self.0.remove_dependency(dep_uuid, ops.as_mut())?) } } diff --git a/taskchampion.pyi b/taskchampion.pyi index 844f340..f587d0d 100644 --- a/taskchampion.pyi +++ b/taskchampion.pyi @@ -8,12 +8,9 @@ class Replica: def new_on_disk(path: str, create_if_missing: bool): ... @staticmethod def new_in_memory(): ... - def create_task(self, uuid: str) -> tuple["Task", list["Operation"]]: ... + def create_task(self, uuid: str, ops: "Operations") -> "Task": ... def all_task_uuids(self) -> list[str]: ... def all_tasks(self) -> dict[str, "Task"]: ... - def update_task( - self, uuid: str, property: str, value: Optional[str] - ) -> dict[str, str]: ... def working_set(self) -> "WorkingSet": ... def dependency_map(self, force: bool) -> "DependencyMap": ... def get_task(self, uuid: str) -> Optional["Task"]: ... @@ -30,16 +27,16 @@ class Replica: avoid_snapshots: bool, ): ... def rebuild_working_set(self, renumber: bool): ... - def add_undo_point(self, force: bool) -> None: ... def num_local_operations(self) -> int: ... def num_undo_points(self) -> int: ... - def commit_operations(self, operations: list["Operation"]) -> None: ... + def commit_operations(self, ops: "Operations") -> None: ... + def commit_reversed_operations(self, ops: "Operations") -> None: ... class Operation: @staticmethod def Create(uuid: str) -> "Operation": ... @staticmethod - def Delete(uuid: str, old_task: dict[str, str]): ... + def Delete(uuid: str, old_task: dict[str, str]) -> "Operation": ... @staticmethod def Update( uuid: str, @@ -50,6 +47,20 @@ class Operation: ) -> "Operation": ... @staticmethod def UndoPoint() -> "Operation": ... + def is_create(self) -> bool: ... + def is_update(self) -> bool: ... + def is_delete(self) -> bool: ... + def is_undo_point(self) -> bool: ... + + uuid: str + old_task: dict[str, str] + timestamp: str + property: Optional[str] + old_value: Optional[str] + value: Optional[str] + +class Operations: + def append(op: "Operation"): ... class Status(Enum): Pending = 1 @@ -58,6 +69,15 @@ class Status(Enum): Recurring = 4 Unknown = 5 +class TaskData: + @staticmethod + def create(uuid: str, ops: "Operations") -> "TaskData": ... + uuid: str + def get(self, value: str) -> Optional[str]: ... + def has(self, value: str) -> bool: ... + def update(self, property: str, value: str, ops: "Operations"): ... + def delete(self, ops: "Operations"): ... + class Task: def get_uuid(self) -> str: ... def get_status(self) -> "Status": ... @@ -78,29 +98,27 @@ class Task: def get_due(self) -> Optional[datetime]: ... def get_dependencies(self) -> list[str]: ... def get_value(self, property: str) -> Optional[str]: ... - def set_status(self, status: "Status") -> list["Operation"]: ... - def set_description(self, description: str) -> list["Operation"]: ... - def set_priority(self, priority: str) -> list["Operation"]: ... - def set_entry(self, entry: Optional[str]) -> list["Operation"]: ... - def set_wait(self, wait: Optional[str]) -> list["Operation"]: ... - def set_modified(self, modified: Optional[str]) -> list["Operation"]: ... - def set_value( - self, property: str, value: Optional[str] - ) -> Optional["Operation"]: ... - def start(self) -> list["Operation"]: ... - def stop(self) -> list["Operation"]: ... - def done(self) -> list["Operation"]: ... - def add_tag(self, tag: "Tag") -> list["Operation"]: ... - def remove_tag(self, tag: "Tag") -> list["Operation"]: ... - def add_annotation(self, annotation: "Annotation") -> list["Operation"]: ... - def remove_annotation(self, annotation: "Annotation") -> list["Operation"]: ... - def set_due(self, due: Optional[datetime]) -> list["Operation"]: ... - def set_uda(self, namespace: str, key: str, value: str) -> list["Operation"]: ... - def remove_uda(self, namespace: str, key: str) -> list["Operation"]: ... - def set_legacy_uda(self, key: str, value: str) -> list["Operation"]: ... - def remove_legacy_uda(self, key: str) -> list["Operation"]: ... - def add_dependency(self, uuid: str) -> list["Operation"]: ... - def remove_dependency(self, uuid: str) -> list["Operation"]: ... + def set_status(self, status: "Status", ops: "Operations"): ... + def set_description(self, description: str, ops: "Operations"): ... + def set_priority(self, priority: str, ops: "Operations"): ... + def set_entry(self, entry: Optional[str], ops: "Operations"): ... + def set_wait(self, wait: Optional[str], ops: "Operations"): ... + def set_modified(self, modified: Optional[str], ops: "Operations"): ... + def set_value(self, property: str, value: Optional[str], ops: "Operations"): ... + def start(self, ops: "Operations"): ... + def stop(self, ops: "Operations"): ... + def done(self, ops: "Operations"): ... + def add_tag(self, tag: "Tag", ops: "Operations"): ... + def remove_tag(self, tag: "Tag", ops: "Operations"): ... + def add_annotation(self, annotation: "Annotation", ops: "Operations"): ... + def remove_annotation(self, annotation: "Annotation", ops: "Operations"): ... + def set_due(self, due: Optional[datetime], ops: "Operations"): ... + def set_uda(self, namespace: str, key: str, value: str, ops: "Operations"): ... + def remove_uda(self, namespace: str, key: str, ops: "Operations"): ... + def set_legacy_uda(self, key: str, value: str, ops: "Operations"): ... + def remove_legacy_uda(self, key: str, ops: "Operations"): ... + def add_dependency(self, uuid: str, ops: "Operations"): ... + def remove_dependency(self, uuid: str, ops: "Operations"): ... class WorkingSet: def __len__(self) -> int: ... diff --git a/tests/test_dependency_map.py b/tests/test_dependency_map.py index d28731b..232a693 100644 --- a/tests/test_dependency_map.py +++ b/tests/test_dependency_map.py @@ -1,6 +1,6 @@ import uuid import pytest -from taskchampion import Replica, TaskData +from taskchampion import Replica, TaskData, Operations def test_dependency_map(): @@ -8,25 +8,17 @@ def test_dependency_map(): u1 = str(uuid.uuid4()) u2 = str(uuid.uuid4()) u3 = str(uuid.uuid4()) - ops = [] + ops = Operations() # Set up t3 depending on t2 depending on t1. - t1, op = TaskData.create(u1) - ops.append(op) - op = t1.update("status", "pending") - ops.append(op) - t2, op = TaskData.create(u2) - ops.append(op) - op = t2.update("status", "pending") - ops.append(op) - op = t2.update(f"dep_{u1}", "x") - ops.append(op) - t3, op = TaskData.create(u3) - ops.append(op) - op = t3.update("status", "pending") - ops.append(op) - op = t3.update(f"dep_{u2}", "x") - ops.append(op) + t1 = TaskData.create(u1, ops) + t1.update("status", "pending", ops) + t2 = TaskData.create(u2, ops) + t2.update("status", "pending", ops) + t2.update(f"dep_{u1}", "x", ops) + t3 = TaskData.create(u3, ops) + t3.update("status", "pending", ops) + t3.update(f"dep_{u2}", "x", ops) r.commit_operations(ops) diff --git a/tests/test_operation.py b/tests/test_operation.py index f446388..09ebc94 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -1,5 +1,106 @@ from taskchampion import Operation +import pytest -def test_new_create(): +def test_create(): o = Operation.Create("10c52749-aec7-4ec9-b390-f371883b9605") + assert repr(o) == "Create { uuid: 10c52749-aec7-4ec9-b390-f371883b9605 }" + assert o.is_create() + assert not o.is_delete() + assert not o.is_update() + assert not o.is_undo_point() + assert o.uuid == "10c52749-aec7-4ec9-b390-f371883b9605" + with pytest.raises(AttributeError): + o.old_task + with pytest.raises(AttributeError): + o.property + with pytest.raises(AttributeError): + o.timestamp + with pytest.raises(AttributeError): + o.old_value + with pytest.raises(AttributeError): + o.value + + +def test_delete(): + o = Operation.Delete("10c52749-aec7-4ec9-b390-f371883b9605", {"foo": "bar"}) + assert ( + repr(o) + == 'Delete { uuid: 10c52749-aec7-4ec9-b390-f371883b9605, old_task: {"foo": "bar"} }' + ) + assert not o.is_create() + assert o.is_delete() + assert not o.is_update() + assert not o.is_undo_point() + assert o.uuid == "10c52749-aec7-4ec9-b390-f371883b9605" + assert o.old_task == {"foo": "bar"} + with pytest.raises(AttributeError): + o.property + with pytest.raises(AttributeError): + o.timestamp + with pytest.raises(AttributeError): + o.old_value + with pytest.raises(AttributeError): + o.value + + +def test_update(): + o = Operation.Update( + "10c52749-aec7-4ec9-b390-f371883b9605", + "foo", + "2038-01-19T03:14:07+00:00", + "old", + "new", + ) + assert ( + repr(o) + == 'Update { uuid: 10c52749-aec7-4ec9-b390-f371883b9605, property: "foo", old_value: Some("old"), value: Some("new"), timestamp: 2038-01-19T03:14:07Z }' + ) + assert not o.is_create() + assert not o.is_delete() + assert o.is_update() + assert not o.is_undo_point() + assert o.uuid == "10c52749-aec7-4ec9-b390-f371883b9605" + with pytest.raises(AttributeError): + o.old_task + assert o.property == "foo" + assert o.timestamp == "2038-01-19 03:14:07 UTC" + assert o.old_value == "old" + assert o.value == "new" + + +def test_update_none(): + o = Operation.Update( + "10c52749-aec7-4ec9-b390-f371883b9605", + "foo", + "2038-01-19T03:14:07+00:00", + None, + None, + ) + assert ( + repr(o) + == 'Update { uuid: 10c52749-aec7-4ec9-b390-f371883b9605, property: "foo", old_value: None, value: None, timestamp: 2038-01-19T03:14:07Z }' + ) + assert o.old_value == None + assert o.value == None + + +def test_undo_point(): + o = Operation.UndoPoint() + assert repr(o) == "UndoPoint" + assert not o.is_create() + assert not o.is_delete() + assert not o.is_update() + assert o.is_undo_point() + with pytest.raises(AttributeError): + o.uuid + with pytest.raises(AttributeError): + o.old_task + with pytest.raises(AttributeError): + o.property + with pytest.raises(AttributeError): + o.timestamp + with pytest.raises(AttributeError): + o.old_value + with pytest.raises(AttributeError): + o.value diff --git a/tests/test_operations.py b/tests/test_operations.py new file mode 100644 index 0000000..cdc69a5 --- /dev/null +++ b/tests/test_operations.py @@ -0,0 +1,53 @@ +from taskchampion import Operation, Operations, TaskData +import uuid +import pytest + + +@pytest.fixture +def all_ops() -> Operations: + "Return Operations containing one of each type of operation." + ops = Operations() + task = TaskData.create(str(uuid.uuid4()), ops) + task.update("foo", "new", ops) + task.delete(ops) + ops.append(Operation.UndoPoint()) + return ops + + +def test_constructor(): + ops = Operations() + assert not ops + assert len(ops) == 0 + + +def test_repr(): + ops = Operations() + assert repr(ops) == "Operations([])" + ops.append(Operation.UndoPoint()) + assert repr(ops) == "Operations([UndoPoint])" + + +def test_len(all_ops: Operations): + assert all_ops + assert len(all_ops) == 4 + + +def test_indexing(all_ops: Operations): + assert all_ops[0].is_create() + assert all_ops[1].is_update() + assert all_ops[2].is_delete() + assert all_ops[3].is_undo_point() + with pytest.raises(IndexError): + all_ops[4] + # For the moment, negative indices are not supported (although pyo3 docs suggest they should work) + with pytest.raises(OverflowError): + all_ops[-1] + + +def test_iteration(all_ops: Operations): + seen_undo_point = False + for op in all_ops: + print(repr(op)) + if op.is_undo_point(): + seen_undo_point = True + assert seen_undo_point diff --git a/tests/test_replica.py b/tests/test_replica.py index 74bf579..4a36301 100644 --- a/tests/test_replica.py +++ b/tests/test_replica.py @@ -2,7 +2,7 @@ from pathlib import Path import pytest -from taskchampion import Replica +from taskchampion import Replica, Operations @pytest.fixture @@ -12,16 +12,10 @@ def empty_replica() -> Replica: @pytest.fixture def replica_with_tasks(empty_replica: Replica): - ops = [] - _, op = empty_replica.create_task(str(uuid.uuid4())) - ops.extend(op) - - _, op = empty_replica.create_task(str(uuid.uuid4())) - ops.extend(op) - - _, op = empty_replica.create_task(str(uuid.uuid4())) - ops.extend(op) - + ops = Operations() + _ = empty_replica.create_task(str(uuid.uuid4()), ops) + _ = empty_replica.create_task(str(uuid.uuid4()), ops) + _ = empty_replica.create_task(str(uuid.uuid4()), ops) empty_replica.commit_operations(ops) return empty_replica @@ -36,8 +30,9 @@ def test_constructor(tmp_path: Path): def test_sync_to_local(tmp_path: Path): u = str(uuid.uuid4()) r = Replica.new_in_memory() - _, op = r.create_task(u) - r.commit_operations(op) + ops = Operations() + r.create_task(u, ops) + r.commit_operations(ops) r.sync_to_local(str(tmp_path), False) # Verify that task syncs to another replica. @@ -55,8 +50,9 @@ def test_constructor_throws_error_with_missing_database(tmp_path: Path): def test_create_task(empty_replica: Replica): u = uuid.uuid4() - _, op = empty_replica.create_task(str(u)) - empty_replica.commit_operations(op) + ops = Operations() + _ = empty_replica.create_task(str(u), ops) + empty_replica.commit_operations(ops) tasks = empty_replica.all_task_uuids() @@ -64,32 +60,20 @@ def test_create_task(empty_replica: Replica): def test_all_task_uuids(empty_replica: Replica): - ops = [] - _, op = empty_replica.create_task(str(uuid.uuid4())) - ops.extend(op) - - _, op = empty_replica.create_task(str(uuid.uuid4())) - ops.extend(op) - - _, op = empty_replica.create_task(str(uuid.uuid4())) - ops.extend(op) - + ops = Operations() + _ = empty_replica.create_task(str(uuid.uuid4()), ops) + _ = empty_replica.create_task(str(uuid.uuid4()), ops) + _ = empty_replica.create_task(str(uuid.uuid4()), ops) empty_replica.commit_operations(ops) tasks = empty_replica.all_task_uuids() assert len(tasks) == 3 def test_all_tasks(empty_replica: Replica): - ops = [] - _, op = empty_replica.create_task(str(uuid.uuid4())) - ops.extend(op) - - _, op = empty_replica.create_task(str(uuid.uuid4())) - ops.extend(op) - - _, op = empty_replica.create_task(str(uuid.uuid4())) - ops.extend(op) - + ops = Operations() + _ = empty_replica.create_task(str(uuid.uuid4()), ops) + _ = empty_replica.create_task(str(uuid.uuid4()), ops) + _ = empty_replica.create_task(str(uuid.uuid4()), ops) empty_replica.commit_operations(ops) tasks = empty_replica.all_tasks() @@ -132,17 +116,18 @@ def test_add_undo_point(replica_with_tasks: Replica): def test_num_local_operations(replica_with_tasks: Replica): assert replica_with_tasks.num_local_operations() == 3 - _, op = replica_with_tasks.create_task(str(uuid.uuid4())) + ops = Operations() + _ = replica_with_tasks.create_task(str(uuid.uuid4()), ops) + replica_with_tasks.commit_operations(ops) - replica_with_tasks.commit_operations(op) assert replica_with_tasks.num_local_operations() == 4 def test_num_undo_points(replica_with_tasks: Replica): assert replica_with_tasks.num_undo_points() == 3 - _, op = replica_with_tasks.create_task(str(uuid.uuid4())) - - replica_with_tasks.commit_operations(op) + ops = Operations() + _ = replica_with_tasks.create_task(str(uuid.uuid4()), ops) + replica_with_tasks.commit_operations(ops) assert replica_with_tasks.num_undo_points() == 4 diff --git a/tests/test_task.py b/tests/test_task.py index db54054..457b6f7 100644 --- a/tests/test_task.py +++ b/tests/test_task.py @@ -1,4 +1,4 @@ -from taskchampion import Task, Replica, Status, Tag +from taskchampion import Task, Replica, Status, Tag, Operations from datetime import datetime import pytest import uuid @@ -7,10 +7,9 @@ @pytest.fixture def new_task(): r = Replica.new_in_memory() - result = r.create_task(str(uuid.uuid4())) - - assert result is not None - task, _ = result + ops = Operations() + task = r.create_task(str(uuid.uuid4()), ops) + r.commit_operations(ops) return task @@ -18,14 +17,12 @@ def new_task(): @pytest.fixture def waiting_task(): r = Replica.new_in_memory() - result = r.create_task(str(uuid.uuid4())) - - assert result is not None - task, _ = result - - task.set_wait("2038-01-19T03:14:07+00:00") - task.set_priority("10") - task.add_tag(Tag("example_tag")) + ops = Operations() + task = r.create_task(str(uuid.uuid4()), ops) + task.set_wait("2038-01-19T03:14:07+00:00", ops) + task.set_priority("10", ops) + task.add_tag(Tag("example_tag"), ops) + r.commit_operations(ops) return task @@ -33,23 +30,20 @@ def waiting_task(): @pytest.fixture def started_task(): r = Replica.new_in_memory() + ops = Operations() + task = r.create_task(str(uuid.uuid4()), ops) + task.start(ops) + r.commit_operations(ops) - result = r.create_task(str(uuid.uuid4())) - assert result is not None - task, _ = result - - task.start() return task @pytest.fixture def blocked_task(): r = Replica.new_in_memory() - result = r.create_task(str(uuid.uuid4())) - - assert result is not None - - task, _ = result + ops = Operations() + task = r.create_task(str(uuid.uuid4()), ops) + r.commit_operations(ops) # Fragile test, but I cannot mock Rust's Chrono, so this will do. # Need to refresh the tag, the one that's in memory is stale @@ -59,9 +53,10 @@ def blocked_task(): @pytest.fixture def due_task(): r = Replica.new_in_memory() - task, _ = r.create_task(str(uuid.uuid4())) - - task.set_due(datetime.fromisoformat("2006-05-13T01:27:27+00:00")) + ops = Operations() + task = r.create_task(str(uuid.uuid4()), ops) + task.set_due(datetime.fromisoformat("2006-05-13T01:27:27+00:00"), ops) + r.commit_operations(ops) # Need to refresh the tag, the one that's in memory is stale return task diff --git a/tests/test_working_set.py b/tests/test_working_set.py index b3b7c18..5769b80 100644 --- a/tests/test_working_set.py +++ b/tests/test_working_set.py @@ -1,4 +1,4 @@ -from taskchampion import Replica, WorkingSet, Status +from taskchampion import Replica, WorkingSet, Status, Operations from pathlib import Path import pytest import uuid @@ -8,16 +8,12 @@ def working_set(): r = Replica.new_in_memory() - ops = [] - task, op = r.create_task(str(uuid.uuid4())) - ops.extend(op) - ops.extend(task.set_status(Status.Pending)) - - task, op = r.create_task(str(uuid.uuid4())) - ops.extend(op) - ops.extend(task.set_status(Status.Pending)) - - ops.extend(task.start()) + ops = Operations() + task = r.create_task(str(uuid.uuid4()), ops) + task.set_status(Status.Pending, ops) + task = r.create_task(str(uuid.uuid4()), ops) + task.set_status(Status.Pending, ops) + task.start(ops) r.commit_operations(ops) return r.working_set()