From 313044081b80734e841f130f73f6283db7dc2a44 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 6 Jan 2025 02:46:58 -0500 Subject: [PATCH] Use PyResult for all return values to Python (#35) In many cases, this performs a similar conversion as that supplied automatically by pyo3, but it also allows raising ValueError for invalid UUIDs instead of the default RuntimeError. This also fixes the type of the `timestamp` property of `Operation.Update`, which was missed when converting other datetimes to use the `datetime`/`DateTime` types. * Temporarily use Poetry 1.x --- .github/workflows/ci.yml | 5 +- .github/workflows/tests.yml | 3 +- Cargo.lock | 2 - Cargo.toml | 3 +- src/lib.rs | 2 + src/operation.rs | 26 +++---- src/replica.rs | 136 ++++++++++++++++++++-------------- src/task/task.rs | 141 ++++++++++++++++++++---------------- src/util.rs | 15 ++++ taskchampion.pyi | 4 +- tests/test_operation.py | 9 ++- 11 files changed, 207 insertions(+), 139 deletions(-) create mode 100644 src/util.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c70e59..6217470 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,7 +191,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: 3.9 - - run: pip install poetry + # See #36 for upgrading to poetry 2.0. + - run: pip install 'poetry<2' - run: poetry install - run: poetry run pip install wheels-linux-x86_64/* - run: poetry run pytest @@ -207,4 +208,4 @@ jobs: python-version: 3.9 - run: pip install wheels-linux-x86_64/* - run: pip install mypy - - run: python -m mypy.stubtest taskchampion --ignore-missing-stub \ No newline at end of file + - run: python -m mypy.stubtest taskchampion --ignore-missing-stub diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c0d44f2..88d914c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,8 @@ jobs: with: python-version: 3.9 - - run: pip install poetry + # See #36 for upgrading to poetry 2.0. + - run: pip install 'poetry<2' - run: poetry install - run: poetry run maturin develop - run: poetry run pytest diff --git a/Cargo.lock b/Cargo.lock index a379199..49e9768 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1672,7 +1672,6 @@ version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" dependencies = [ - "anyhow", "cfg-if", "chrono", "indoc", @@ -2393,7 +2392,6 @@ dependencies = [ name = "taskchampion-py" version = "1.0.2" dependencies = [ - "anyhow", "chrono", "pyo3", "taskchampion", diff --git a/Cargo.toml b/Cargo.toml index 9d926a7..fdf2e92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ crate-type = ["cdylib"] doc = false [dependencies] -pyo3 = { version = "0.22.6", features = ["anyhow", "chrono"] } +pyo3 = { version = "0.22.6", features = ["chrono"] } chrono = "*" -anyhow = "*" taskchampion = { version = "=1.0.2" } diff --git a/src/lib.rs b/src/lib.rs index 897f0a4..8b85854 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,8 @@ use operations::*; mod task; use task::{Annotation, Status, Tag, Task, TaskData}; +mod util; + #[pymodule] fn taskchampion(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; diff --git a/src/operation.rs b/src/operation.rs index 42cd060..97f901e 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -1,8 +1,8 @@ -use chrono::DateTime; +use crate::util::uuid2tc; +use chrono::{DateTime, Utc}; use pyo3::{exceptions::PyAttributeError, prelude::*}; - use std::collections::HashMap; -use taskchampion::{Operation as TCOperation, Uuid}; +use taskchampion::Operation as TCOperation; #[pyclass] #[derive(PartialEq, Eq, Clone, Debug)] @@ -17,17 +17,17 @@ pub struct Operation(pub(crate) TCOperation); impl Operation { #[allow(non_snake_case)] #[staticmethod] - pub fn Create(uuid: String) -> anyhow::Result { + pub fn Create(uuid: String) -> PyResult { Ok(Operation(TCOperation::Create { - uuid: Uuid::parse_str(&uuid)?, + uuid: uuid2tc(uuid)?, })) } #[allow(non_snake_case)] #[staticmethod] - pub fn Delete(uuid: String, old_task: HashMap) -> anyhow::Result { + pub fn Delete(uuid: String, old_task: HashMap) -> PyResult { Ok(Operation(TCOperation::Delete { - uuid: Uuid::parse_str(&uuid)?, + uuid: uuid2tc(uuid)?, old_task, })) } @@ -38,16 +38,16 @@ impl Operation { pub fn Update( uuid: String, property: String, - timestamp: String, + timestamp: DateTime, old_value: Option, value: Option, - ) -> anyhow::Result { + ) -> PyResult { Ok(Operation(TCOperation::Update { - uuid: Uuid::parse_str(&uuid)?, + uuid: uuid2tc(uuid)?, property, old_value, value, - timestamp: DateTime::parse_from_rfc3339(×tamp).unwrap().into(), + timestamp, })) } @@ -113,10 +113,10 @@ impl Operation { } #[getter(timestamp)] - pub fn get_timestamp(&self) -> PyResult { + pub fn get_timestamp(&self) -> PyResult> { use TCOperation::*; match &self.0 { - Update { timestamp, .. } => Ok(timestamp.to_string()), + Update { timestamp, .. } => Ok(*timestamp), _ => Err(PyAttributeError::new_err( "Variant does not have attribute 'timestamp'", )), diff --git a/src/replica.rs b/src/replica.rs index 1d2cd2f..932b332 100644 --- a/src/replica.rs +++ b/src/replica.rs @@ -1,10 +1,10 @@ -use std::collections::HashMap; -use std::rc::Rc; - use crate::task::TaskData; +use crate::util::{into_runtime_error, uuid2tc}; use crate::{DependencyMap, Operations, Task, WorkingSet}; use pyo3::prelude::*; -use taskchampion::{Replica as TCReplica, ServerConfig, StorageConfig, Uuid}; +use std::collections::HashMap; +use std::rc::Rc; +use taskchampion::{Replica as TCReplica, ServerConfig, StorageConfig}; #[pyclass(unsendable)] /// A replica represents an instance of a user's task data, providing an easy interface @@ -24,64 +24,73 @@ impl Replica { /// create_if_missing (bool): create the database if it does not exist /// Raises: /// RuntimeError: if database does not exist, and create_if_missing is false - pub fn new_on_disk(path: String, create_if_missing: bool) -> anyhow::Result { + pub fn new_on_disk(path: String, create_if_missing: bool) -> PyResult { Ok(Replica(TCReplica::new( StorageConfig::OnDisk { taskdb_dir: path.into(), create_if_missing, } - .into_storage()?, + .into_storage() + .map_err(into_runtime_error)?, ))) } #[staticmethod] - pub fn new_in_memory() -> anyhow::Result { + pub fn new_in_memory() -> PyResult { Ok(Replica(TCReplica::new( - StorageConfig::InMemory.into_storage()?, + StorageConfig::InMemory + .into_storage() + .map_err(into_runtime_error)?, ))) } /// Create a new task /// The task must not already exist. - pub fn create_task(&mut self, uuid: String, ops: &mut Operations) -> anyhow::Result { + pub fn create_task(&mut self, uuid: String, ops: &mut Operations) -> PyResult { let task = self .0 - .create_task(Uuid::parse_str(&uuid)?, ops.as_mut()) - .map(Task::from)?; + .create_task(uuid2tc(uuid)?, ops.as_mut()) + .map_err(into_runtime_error)? + .into(); Ok(task) } /// Get a list of all tasks in the replica. - pub fn all_tasks(&mut self) -> anyhow::Result> { + pub fn all_tasks(&mut self) -> PyResult> { Ok(self .0 - .all_tasks()? + .all_tasks() + .map_err(into_runtime_error)? .into_iter() .map(|(key, value)| (key.to_string(), value.into())) .collect()) } - pub fn all_task_data(&mut self) -> anyhow::Result> { + pub fn all_task_data(&mut self) -> PyResult> { Ok(self .0 - .all_task_data()? + .all_task_data() + .map_err(into_runtime_error)? .into_iter() .map(|(key, value)| (key.to_string(), TaskData::from(value))) .collect()) } /// Get a list of all uuids for tasks in the replica. - pub fn all_task_uuids(&mut self) -> anyhow::Result> { + pub fn all_task_uuids(&mut self) -> PyResult> { Ok(self .0 .all_task_uuids() - .map(|v| v.iter().map(|item| item.to_string()).collect())?) + .map_err(into_runtime_error)? + .iter() + .map(|item| item.to_string()) + .collect()) } - pub fn working_set(&mut self) -> anyhow::Result { - Ok(self.0.working_set().map(WorkingSet::from)?) + pub fn working_set(&mut self) -> PyResult { + Ok(self.0.working_set().map_err(into_runtime_error)?.into()) } - pub fn dependency_map(&mut self, force: bool) -> anyhow::Result { + pub fn dependency_map(&mut self, force: bool) -> PyResult { // `Rc` is not thread-safe, so we must get an owned copy of the data it contains. // Unfortunately, it cannot be cloned, so this is impossible (but both issues are fixed in // https://github.com/GothenburgBitFactory/taskchampion/pull/514). @@ -91,37 +100,44 @@ impl Replica { // that TaskChampion does not modify a DependencyMap after creating it. // // This is a temporary hack, and should not be used in "real" code! - let dm = self.0.dependency_map(force)?; + let dm = self.0.dependency_map(force).map_err(into_runtime_error)?; // NOTE: this does not decrement the reference count and thus "leaks" the Rc. let dm_ptr = Rc::into_raw(dm); Ok(dm_ptr.into()) } - pub fn get_task(&mut self, uuid: String) -> anyhow::Result> { + pub fn get_task(&mut self, uuid: String) -> PyResult> { Ok(self .0 - .get_task(Uuid::parse_str(&uuid).unwrap()) - .map(|opt| opt.map(Task::from))?) + .get_task(uuid2tc(uuid)?) + .map_err(into_runtime_error)? + .map(|t| t.into())) } - pub fn get_task_data(&mut self, uuid: String) -> anyhow::Result> { + pub fn get_task_data(&mut self, uuid: String) -> PyResult> { Ok(self .0 - .get_task_data(Uuid::parse_str(&uuid)?) - .map(|opt| opt.map(TaskData::from))?) + .get_task_data(uuid2tc(uuid)?) + .map_err(into_runtime_error)? + .map(TaskData::from)) + } + + pub fn commit_operations(&mut self, ops: Operations) -> PyResult<()> { + self.0 + .commit_operations(ops.into()) + .map_err(into_runtime_error) } /// Sync with a server crated from `ServerConfig::Local`. - fn sync_to_local(&mut self, server_dir: String, avoid_snapshots: bool) -> anyhow::Result<()> { + fn sync_to_local(&mut self, server_dir: String, avoid_snapshots: bool) -> PyResult<()> { let mut server = ServerConfig::Local { server_dir: server_dir.into(), } - .into_server()?; - 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())?) + .into_server() + .map_err(into_runtime_error)?; + self.0 + .sync(&mut server, avoid_snapshots) + .map_err(into_runtime_error) } /// Sync with a server created from `ServerConfig::Remote`. @@ -131,14 +147,17 @@ impl Replica { client_id: String, encryption_secret: String, avoid_snapshots: bool, - ) -> anyhow::Result<()> { + ) -> PyResult<()> { let mut server = ServerConfig::Remote { url, - client_id: Uuid::parse_str(&client_id)?, + client_id: uuid2tc(client_id)?, encryption_secret: encryption_secret.into(), } - .into_server()?; - Ok(self.0.sync(&mut server, avoid_snapshots)?) + .into_server() + .map_err(into_runtime_error)?; + self.0 + .sync(&mut server, avoid_snapshots) + .map_err(into_runtime_error) } /// Sync with a server created from `ServerConfig::Gcp`. @@ -149,37 +168,48 @@ impl Replica { credential_path: Option, encryption_secret: String, avoid_snapshots: bool, - ) -> anyhow::Result<()> { + ) -> PyResult<()> { let mut server = ServerConfig::Gcp { bucket, credential_path, encryption_secret: encryption_secret.into(), } - .into_server()?; - Ok(self.0.sync(&mut server, avoid_snapshots)?) + .into_server() + .map_err(into_runtime_error)?; + self.0 + .sync(&mut server, avoid_snapshots) + .map_err(into_runtime_error) } - pub fn rebuild_working_set(&mut self, renumber: bool) -> anyhow::Result<()> { - Ok(self.0.rebuild_working_set(renumber)?) + pub fn rebuild_working_set(&mut self, renumber: bool) -> PyResult<()> { + self.0 + .rebuild_working_set(renumber) + .map_err(into_runtime_error) } - pub fn num_local_operations(&mut self) -> anyhow::Result { - Ok(self.0.num_local_operations()?) + pub fn num_local_operations(&mut self) -> PyResult { + self.0.num_local_operations().map_err(into_runtime_error) } - pub fn num_undo_points(&mut self) -> anyhow::Result { - Ok(self.0.num_local_operations()?) + pub fn num_undo_points(&mut self) -> PyResult { + self.0.num_local_operations().map_err(into_runtime_error) } - pub fn get_undo_operations(&mut self) -> anyhow::Result { - Ok(self.0.get_undo_operations()?.into()) + pub fn get_undo_operations(&mut self) -> PyResult { + Ok(self + .0 + .get_undo_operations() + .map_err(into_runtime_error)? + .into()) } - pub fn commit_reversed_operations(&mut self, operations: Operations) -> anyhow::Result { - Ok(self.0.commit_reversed_operations(operations.into())?) + pub fn commit_reversed_operations(&mut self, operations: Operations) -> PyResult { + self.0 + .commit_reversed_operations(operations.into()) + .map_err(into_runtime_error) } - pub fn expire_tasks(&mut self) -> anyhow::Result<()> { - Ok(self.0.expire_tasks()?) + pub fn expire_tasks(&mut self) -> PyResult<()> { + self.0.expire_tasks().map_err(into_runtime_error) } } diff --git a/src/task/task.rs b/src/task/task.rs index 7fd7cf4..56e97bd 100644 --- a/src/task/task.rs +++ b/src/task/task.rs @@ -1,8 +1,9 @@ use crate::task::{Annotation, Status, Tag, TaskData}; +use crate::util::{into_runtime_error, uuid2tc}; use crate::Operations; use chrono::{DateTime, Utc}; use pyo3::prelude::*; -use taskchampion::{Task as TCTask, Uuid}; +use taskchampion::Task as TCTask; // TODO: This type can be send once https://github.com/GothenburgBitFactory/taskchampion/pull/514 // is available. @@ -184,20 +185,22 @@ impl Task { self.0.get_value(property) } - 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_status(&mut self, status: Status, ops: &mut Operations) -> PyResult<()> { + self.0 + .set_status(status.into(), ops.as_mut()) + .map_err(into_runtime_error) } - 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_description(&mut self, description: String, ops: &mut Operations) -> PyResult<()> { + self.0 + .set_description(description, ops.as_mut()) + .map_err(into_runtime_error) } - pub fn set_priority(&mut self, priority: String, ops: &mut Operations) -> anyhow::Result<()> { - Ok(self.0.set_priority(priority, ops.as_mut())?) + pub fn set_priority(&mut self, priority: String, ops: &mut Operations) -> PyResult<()> { + self.0 + .set_priority(priority, ops.as_mut()) + .map_err(into_runtime_error) } #[pyo3(signature=(entry, ops))] @@ -205,26 +208,24 @@ impl Task { &mut self, entry: Option>, ops: &mut Operations, - ) -> anyhow::Result<()> { - Ok(self.0.set_entry(entry, ops.as_mut())?) + ) -> PyResult<()> { + self.0 + .set_entry(entry, ops.as_mut()) + .map_err(into_runtime_error) } #[pyo3(signature=(wait, ops))] - pub fn set_wait( - &mut self, - wait: Option>, - ops: &mut Operations, - ) -> anyhow::Result<()> { - Ok(self.0.set_wait(wait, ops.as_mut())?) + pub fn set_wait(&mut self, wait: Option>, ops: &mut Operations) -> PyResult<()> { + self.0 + .set_wait(wait, ops.as_mut()) + .map_err(into_runtime_error) } #[pyo3(signature=(modified, ops))] - pub fn set_modified( - &mut self, - modified: DateTime, - ops: &mut Operations, - ) -> anyhow::Result<()> { - Ok(self.0.set_modified(modified, ops.as_mut())?) + pub fn set_modified(&mut self, modified: DateTime, ops: &mut Operations) -> PyResult<()> { + self.0 + .set_modified(modified, ops.as_mut()) + .map_err(into_runtime_error) } #[pyo3(signature=(property, value, ops))] @@ -233,56 +234,64 @@ impl Task { property: String, value: Option, ops: &mut Operations, - ) -> anyhow::Result<()> { - Ok(self.0.set_value(property, value, ops.as_mut())?) + ) -> PyResult<()> { + self.0 + .set_value(property, value, ops.as_mut()) + .map_err(into_runtime_error) } - pub fn start(&mut self, ops: &mut Operations) -> anyhow::Result<()> { - Ok(self.0.start(ops.as_mut())?) + pub fn start(&mut self, ops: &mut Operations) -> PyResult<()> { + self.0.start(ops.as_mut()).map_err(into_runtime_error) } - pub fn stop(&mut self, ops: &mut Operations) -> anyhow::Result<()> { - Ok(self.0.stop(ops.as_mut())?) + pub fn stop(&mut self, ops: &mut Operations) -> PyResult<()> { + self.0.stop(ops.as_mut()).map_err(into_runtime_error) } - pub fn done(&mut self, ops: &mut Operations) -> anyhow::Result<()> { - Ok(self.0.done(ops.as_mut())?) + pub fn done(&mut self, ops: &mut Operations) -> PyResult<()> { + self.0.done(ops.as_mut()).map_err(into_runtime_error) } - 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 add_tag(&mut self, tag: &Tag, ops: &mut Operations) -> PyResult<()> { + self.0 + .add_tag(tag.as_ref(), ops.as_mut()) + .map_err(into_runtime_error) } - 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 remove_tag(&mut self, tag: &Tag, ops: &mut Operations) -> PyResult<()> { + self.0 + .remove_tag(tag.as_ref(), ops.as_mut()) + .map_err(into_runtime_error) } pub fn add_annotation( &mut self, annotation: &Annotation, ops: &mut Operations, - ) -> anyhow::Result<()> { + ) -> PyResult<()> { // Create an owned annotation (TODO: not needed once // https://github.com/GothenburgBitFactory/taskchampion/pull/517 is available) let annotation = Annotation::new(annotation.entry(), annotation.description()); - Ok(self.0.add_annotation(annotation.into(), ops.as_mut())?) + self.0 + .add_annotation(annotation.into(), ops.as_mut()) + .map_err(into_runtime_error) } pub fn remove_annotation( &mut self, timestamp: DateTime, ops: &mut Operations, - ) -> anyhow::Result<()> { - Ok(self.0.remove_annotation(timestamp, ops.as_mut())?) + ) -> PyResult<()> { + self.0 + .remove_annotation(timestamp, ops.as_mut()) + .map_err(into_runtime_error) } #[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_due(&mut self, due: Option>, ops: &mut Operations) -> PyResult<()> { + self.0 + .set_due(due, ops.as_mut()) + .map_err(into_runtime_error) } pub fn set_uda( @@ -291,8 +300,10 @@ impl Task { key: String, value: String, ops: &mut Operations, - ) -> anyhow::Result<()> { - Ok(self.0.set_uda(namespace, key, value, ops.as_mut())?) + ) -> PyResult<()> { + self.0 + .set_uda(namespace, key, value, ops.as_mut()) + .map_err(into_runtime_error) } pub fn remove_uda( @@ -300,8 +311,10 @@ impl Task { namespace: String, key: String, ops: &mut Operations, - ) -> anyhow::Result<()> { - Ok(self.0.remove_uda(namespace, key, ops.as_mut())?) + ) -> PyResult<()> { + self.0 + .remove_uda(namespace, key, ops.as_mut()) + .map_err(into_runtime_error) } pub fn set_legacy_uda( @@ -309,22 +322,28 @@ impl Task { key: String, value: String, ops: &mut Operations, - ) -> anyhow::Result<()> { - Ok(self.0.set_legacy_uda(key, value, ops.as_mut())?) + ) -> PyResult<()> { + self.0 + .set_legacy_uda(key, value, ops.as_mut()) + .map_err(into_runtime_error) } - 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 remove_legacy_uda(&mut self, key: String, ops: &mut Operations) -> PyResult<()> { + self.0 + .remove_legacy_uda(key, ops.as_mut()) + .map_err(into_runtime_error) } - 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 add_dependency(&mut self, dep: String, ops: &mut Operations) -> PyResult<()> { + self.0 + .add_dependency(uuid2tc(dep)?, ops.as_mut()) + .map_err(into_runtime_error) } - 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())?) + pub fn remove_dependency(&mut self, dep: String, ops: &mut Operations) -> PyResult<()> { + self.0 + .remove_dependency(uuid2tc(dep)?, ops.as_mut()) + .map_err(into_runtime_error) } } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..9926088 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,15 @@ +use pyo3::{ + exceptions::{PyRuntimeError, PyValueError}, + prelude::*, +}; +use taskchampion::Uuid; + +/// Covert a strong from Python into a Rust Uuid. +pub(crate) fn uuid2tc(s: impl AsRef) -> PyResult { + Uuid::parse_str(s.as_ref()).map_err(|_| PyValueError::new_err("Invalid UUID")) +} + +/// Convert an anyhow::Error into a Python RuntimeError. +pub(crate) fn into_runtime_error(err: taskchampion::Error) -> PyErr { + PyRuntimeError::new_err(err.to_string()) +} diff --git a/taskchampion.pyi b/taskchampion.pyi index ba74f8f..db93659 100644 --- a/taskchampion.pyi +++ b/taskchampion.pyi @@ -58,7 +58,7 @@ class Operation: def Update( uuid: str, property: str, - timestamp: str, + timestamp: datetime, old_value: Optional[str] = None, value: Optional[str] = None, ) -> "Operation": ... @@ -71,7 +71,7 @@ class Operation: uuid: str old_task: dict[str, str] - timestamp: str + timestamp: datetime property: Optional[str] old_value: Optional[str] value: Optional[str] diff --git a/tests/test_operation.py b/tests/test_operation.py index 09ebc94..ef377d5 100644 --- a/tests/test_operation.py +++ b/tests/test_operation.py @@ -1,4 +1,5 @@ from taskchampion import Operation +from datetime import datetime import pytest @@ -45,10 +46,11 @@ def test_delete(): def test_update(): + ts = datetime.fromisoformat("2038-01-19T03:14:07+00:00") o = Operation.Update( "10c52749-aec7-4ec9-b390-f371883b9605", "foo", - "2038-01-19T03:14:07+00:00", + ts, "old", "new", ) @@ -64,16 +66,17 @@ def test_update(): with pytest.raises(AttributeError): o.old_task assert o.property == "foo" - assert o.timestamp == "2038-01-19 03:14:07 UTC" + assert o.timestamp == ts assert o.old_value == "old" assert o.value == "new" def test_update_none(): + ts = datetime.fromisoformat("2038-01-19T03:14:07+00:00") o = Operation.Update( "10c52749-aec7-4ec9-b390-f371883b9605", "foo", - "2038-01-19T03:14:07+00:00", + ts, None, None, )