From 1af51660faae1a03564ac5ed9b7c8f178df964ef Mon Sep 17 00:00:00 2001 From: Illya Date: Sat, 4 May 2024 16:05:49 +0300 Subject: [PATCH 01/20] feat: POC Python bindings with Rust --- Cargo.lock | 104 ++++++++++++++++++ Cargo.toml | 12 +-- py-lib/.github/workflows/CI.yml | 138 ++++++++++++++++++++++++ py-lib/.gitignore | 180 ++++++++++++++++++++++++++++++++ py-lib/Cargo.toml | 14 +++ py-lib/pyproject.toml | 15 +++ py-lib/src/lib.rs | 10 ++ py-lib/src/replica.rs | 63 +++++++++++ 8 files changed, 529 insertions(+), 7 deletions(-) create mode 100644 py-lib/.github/workflows/CI.yml create mode 100644 py-lib/.gitignore create mode 100644 py-lib/Cargo.toml create mode 100644 py-lib/pyproject.toml create mode 100644 py-lib/src/lib.rs create mode 100644 py-lib/src/replica.rs diff --git a/Cargo.lock b/Cargo.lock index ed90c9121..b9ab4cf4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -772,6 +772,12 @@ dependencies = [ "hashbrown 0.11.2", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "instant" version = "0.1.12" @@ -929,6 +935,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.16" @@ -1105,6 +1120,12 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -1150,6 +1171,77 @@ dependencies = [ "unarray", ] +[[package]] +name = "py-lib" +version = "0.1.0" +dependencies = [ + "pyo3", + "taskchampion", +] + +[[package]] +name = "pyo3" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.18", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.18", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -1629,6 +1721,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + [[package]] name = "taskchampion" version = "0.5.0" @@ -1883,6 +1981,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index d3035dcb0..925c2ebe2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,6 @@ [workspace] -members = [ - "taskchampion", - "lib", - "integration-tests", - "xtask", -] +members = ["taskchampion", "lib", "integration-tests", "xtask", "py-lib"] resolver = "2" @@ -18,7 +13,10 @@ cc = "1.0.73" chrono = { version = "^0.4.22", features = ["serde"] } ffizz-header = "0.5" flate2 = "1" -google-cloud-storage = { version = "0.15.0", default-features = false, features = ["rustls-tls", "auth"] } +google-cloud-storage = { version = "0.15.0", default-features = false, features = [ + "rustls-tls", + "auth", +] } lazy_static = "1" libc = "0.2.136" log = "^0.4.17" diff --git a/py-lib/.github/workflows/CI.yml b/py-lib/.github/workflows/CI.yml new file mode 100644 index 000000000..f88471122 --- /dev/null +++ b/py-lib/.github/workflows/CI.yml @@ -0,0 +1,138 @@ +# This file is autogenerated by maturin v1.5.1 +# To update, run +# +# maturin generate-ci github +# +name: CI + +on: + push: + branches: + - main + - master + tags: + - '*' + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + linux: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: ubuntu-latest + target: x86_64 + - runner: ubuntu-latest + target: x86 + - runner: ubuntu-latest + target: aarch64 + - runner: ubuntu-latest + target: armv7 + - runner: ubuntu-latest + target: s390x + - runner: ubuntu-latest + target: ppc64le + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + manylinux: auto + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-linux-${{ matrix.platform.target }} + path: dist + + windows: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: windows-latest + target: x64 + - runner: windows-latest + target: x86 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + architecture: ${{ matrix.platform.target }} + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-windows-${{ matrix.platform.target }} + path: dist + + macos: + runs-on: ${{ matrix.platform.runner }} + strategy: + matrix: + platform: + - runner: macos-latest + target: x86_64 + - runner: macos-14 + target: aarch64 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.platform.target }} + args: --release --out dist --find-interpreter + sccache: 'true' + - name: Upload wheels + uses: actions/upload-artifact@v4 + with: + name: wheels-macos-${{ matrix.platform.target }} + path: dist + + sdist: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + - name: Upload sdist + uses: actions/upload-artifact@v4 + with: + name: wheels-sdist + path: dist + + release: + name: Release + runs-on: ubuntu-latest + if: "startsWith(github.ref, 'refs/tags/')" + needs: [linux, windows, macos, sdist] + steps: + - uses: actions/download-artifact@v4 + - name: Publish to PyPI + uses: PyO3/maturin-action@v1 + env: + MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + command: upload + args: --non-interactive --skip-existing wheels-*/* diff --git a/py-lib/.gitignore b/py-lib/.gitignore new file mode 100644 index 000000000..00a1af849 --- /dev/null +++ b/py-lib/.gitignore @@ -0,0 +1,180 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,direnv +# Edit at https://www.toptal.com/developers/gitignore?templates=python,direnv + +### direnv ### +.direnv +.envrc + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python,direnv diff --git a/py-lib/Cargo.toml b/py-lib/Cargo.toml new file mode 100644 index 000000000..7f6424f5a --- /dev/null +++ b/py-lib/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "py-lib" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +name = "py_lib" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = "0.20.0" + +taskchampion = { path = "../taskchampion", version = "0.5.0" } diff --git a/py-lib/pyproject.toml b/py-lib/pyproject.toml new file mode 100644 index 000000000..feb4ed891 --- /dev/null +++ b/py-lib/pyproject.toml @@ -0,0 +1,15 @@ +[build-system] +requires = ["maturin>=1.5,<2.0"] +build-backend = "maturin" + +[project] +name = "py-lib" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dynamic = ["version"] +[tool.maturin] +features = ["pyo3/extension-module"] diff --git a/py-lib/src/lib.rs b/py-lib/src/lib.rs new file mode 100644 index 000000000..75bbe79c5 --- /dev/null +++ b/py-lib/src/lib.rs @@ -0,0 +1,10 @@ +pub mod replica; +use pyo3::prelude::*; +use replica::{TCReplica, TCStatus}; +/// Formats the sum of two numbers as string. +#[pymodule] +fn py_lib(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/py-lib/src/replica.rs b/py-lib/src/replica.rs new file mode 100644 index 000000000..e1caf9127 --- /dev/null +++ b/py-lib/src/replica.rs @@ -0,0 +1,63 @@ +use pyo3::{exceptions::PyOSError, prelude::*}; +use std::convert::From; +use taskchampion::{storage::SqliteStorage, Replica, Status}; +#[pyclass] +pub struct TCReplica(Replica); + +unsafe impl Send for TCReplica {} + +#[pymethods] +impl TCReplica { + #[new] + pub fn new(path: String, exists: bool) -> PyResult { + let storage = SqliteStorage::new(path, exists); + match storage { + Ok(v) => Ok(TCReplica(Replica::new(Box::new(v)))), + Err(e) => Err(PyOSError::new_err(e.to_string())), + } + } + pub fn new_task(&mut self, status: TCStatus, description: String) { + let _ = self.0.new_task(status.into(), description); + } + + pub fn all_task_uuids(&mut self) -> PyResult> { + match self.0.all_task_uuids() { + Ok(r) => Ok(r.iter().map(|uuid| uuid.to_string()).collect()), + Err(e) => Err(PyOSError::new_err(e.to_string())), + } + } +} + +#[pyclass] +#[derive(Clone, Copy)] +pub enum TCStatus { + Pending, + Completed, + Deleted, + Recurring, + Unknown, +} + +impl From for Status { + fn from(status: TCStatus) -> Self { + match status { + TCStatus::Pending => Status::Pending, + TCStatus::Completed => Status::Completed, + TCStatus::Deleted => Status::Deleted, + TCStatus::Recurring => Status::Recurring, + _ => Status::Unknown(format!("unknown TCStatus {}", status as u32)), + } + } +} + +impl From for TCStatus { + fn from(status: Status) -> Self { + match status { + Status::Pending => TCStatus::Pending, + Status::Completed => TCStatus::Completed, + Status::Deleted => TCStatus::Deleted, + Status::Recurring => TCStatus::Recurring, + Status::Unknown(_) => TCStatus::Unknown, + } + } +} From 8b7799db9e8292899924c27aeea2e62e6e08286f Mon Sep 17 00:00:00 2001 From: Illya Date: Mon, 6 May 2024 19:29:13 +0300 Subject: [PATCH 02/20] feat: Add tests, cleanup for py_lib - add python tests to test the library implementation - split single file into multiple ones --- .gitignore | 1 + Cargo.lock | 16 ++++----- py-lib/Cargo.toml | 6 ++-- py-lib/notes.md | 4 +++ py-lib/pyproject.toml | 13 +++++++- py-lib/src/lib.rs | 17 ++++++---- py-lib/src/replica.rs | 63 +++++++++++++----------------------- py-lib/src/status.rs | 37 +++++++++++++++++++++ py-lib/src/task.rs | 10 ++++++ py-lib/tests/__init__.py | 0 py-lib/tests/test_replica.py | 15 +++++++++ py-lib/tests/test_status.py | 0 py-lib/tests/test_task.py | 0 13 files changed, 125 insertions(+), 57 deletions(-) create mode 100644 py-lib/notes.md create mode 100644 py-lib/src/status.rs create mode 100644 py-lib/src/task.rs create mode 100644 py-lib/tests/__init__.py create mode 100644 py-lib/tests/test_replica.py create mode 100644 py-lib/tests/test_status.py create mode 100644 py-lib/tests/test_task.py diff --git a/.gitignore b/.gitignore index f04f2a312..595e22abf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ **/*.rs.bk target/ +.venv/ diff --git a/Cargo.lock b/Cargo.lock index b9ab4cf4a..e58ab5ab7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1171,14 +1171,6 @@ dependencies = [ "unarray", ] -[[package]] -name = "py-lib" -version = "0.1.0" -dependencies = [ - "pyo3", - "taskchampion", -] - [[package]] name = "pyo3" version = "0.20.3" @@ -1765,6 +1757,14 @@ dependencies = [ "taskchampion", ] +[[package]] +name = "taskchampion_python" +version = "0.1.0" +dependencies = [ + "pyo3", + "taskchampion", +] + [[package]] name = "tempfile" version = "3.6.0" diff --git a/py-lib/Cargo.toml b/py-lib/Cargo.toml index 7f6424f5a..f380e17d0 100644 --- a/py-lib/Cargo.toml +++ b/py-lib/Cargo.toml @@ -1,11 +1,13 @@ [package] -name = "py-lib" +name = "taskchampion_python" version = "0.1.0" edition = "2021" +[package.metadata.maturin] +name = "taskchampion" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] -name = "py_lib" +name = "taskchampion" crate-type = ["cdylib"] [dependencies] diff --git a/py-lib/notes.md b/py-lib/notes.md new file mode 100644 index 000000000..c97ad7ba4 --- /dev/null +++ b/py-lib/notes.md @@ -0,0 +1,4 @@ +# Notes while developing the project + +- renamed the package to taskchampion, instead of py_lib, so the python imports work nicely + diff --git a/py-lib/pyproject.toml b/py-lib/pyproject.toml index feb4ed891..4149728de 100644 --- a/py-lib/pyproject.toml +++ b/py-lib/pyproject.toml @@ -3,7 +3,7 @@ requires = ["maturin>=1.5,<2.0"] build-backend = "maturin" [project] -name = "py-lib" +name = "taskchampion" requires-python = ">=3.8" classifiers = [ "Programming Language :: Rust", @@ -11,5 +11,16 @@ classifiers = [ "Programming Language :: Python :: Implementation :: PyPy", ] dynamic = ["version"] + [tool.maturin] features = ["pyo3/extension-module"] + +[tool.poetry] +name = "taskchampion" +version = "0.1.0" +authors = ["illyalaifu "] +description = "" + +[tool.poetry.dependencies] +python = ">=3.8" +pytest = "*" diff --git a/py-lib/src/lib.rs b/py-lib/src/lib.rs index 75bbe79c5..8cba1005d 100644 --- a/py-lib/src/lib.rs +++ b/py-lib/src/lib.rs @@ -1,10 +1,15 @@ -pub mod replica; use pyo3::prelude::*; -use replica::{TCReplica, TCStatus}; -/// Formats the sum of two numbers as string. +pub mod replica; +use replica::*; +pub mod status; +use status::*; +pub mod task; +use task::*; + #[pymodule] -fn py_lib(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; +fn taskchampion(_py: Python, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + Ok(()) } diff --git a/py-lib/src/replica.rs b/py-lib/src/replica.rs index e1caf9127..fa1848f6a 100644 --- a/py-lib/src/replica.rs +++ b/py-lib/src/replica.rs @@ -1,22 +1,30 @@ +use std::collections::HashMap; + +use crate::status::Status; +use crate::Task; use pyo3::{exceptions::PyOSError, prelude::*}; -use std::convert::From; -use taskchampion::{storage::SqliteStorage, Replica, Status}; +use taskchampion::storage::SqliteStorage; +use taskchampion::Replica as TCReplica; + #[pyclass] -pub struct TCReplica(Replica); +/// A replica represents an instance of a user's task data, providing an easy interface +/// for querying and modifying that data. +pub struct Replica(TCReplica); -unsafe impl Send for TCReplica {} +unsafe impl Send for Replica {} #[pymethods] -impl TCReplica { +impl Replica { #[new] - pub fn new(path: String, exists: bool) -> PyResult { + pub fn new(path: String, exists: bool) -> PyResult { let storage = SqliteStorage::new(path, exists); + // TODO convert this and other match Result into ? for less boilerplate. match storage { - Ok(v) => Ok(TCReplica(Replica::new(Box::new(v)))), + Ok(v) => Ok(Replica(TCReplica::new(Box::new(v)))), Err(e) => Err(PyOSError::new_err(e.to_string())), } } - pub fn new_task(&mut self, status: TCStatus, description: String) { + pub fn new_task(&mut self, status: Status, description: String) { let _ = self.0.new_task(status.into(), description); } @@ -26,38 +34,13 @@ impl TCReplica { Err(e) => Err(PyOSError::new_err(e.to_string())), } } -} - -#[pyclass] -#[derive(Clone, Copy)] -pub enum TCStatus { - Pending, - Completed, - Deleted, - Recurring, - Unknown, -} - -impl From for Status { - fn from(status: TCStatus) -> Self { - match status { - TCStatus::Pending => Status::Pending, - TCStatus::Completed => Status::Completed, - TCStatus::Deleted => Status::Deleted, - TCStatus::Recurring => Status::Recurring, - _ => Status::Unknown(format!("unknown TCStatus {}", status as u32)), - } - } -} - -impl From for TCStatus { - fn from(status: Status) -> Self { - match status { - Status::Pending => TCStatus::Pending, - Status::Completed => TCStatus::Completed, - Status::Deleted => TCStatus::Deleted, - Status::Recurring => TCStatus::Recurring, - Status::Unknown(_) => TCStatus::Unknown, + pub fn all_tasks(&mut self) -> PyResult> { + match self.0.all_tasks() { + Ok(v) => Ok(v + .into_iter() + .map(|(key, value)| (key.to_string(), Task(value))) + .collect()), + Err(e) => Err(PyOSError::new_err(e.to_string())), } } } diff --git a/py-lib/src/status.rs b/py-lib/src/status.rs new file mode 100644 index 000000000..933a92af5 --- /dev/null +++ b/py-lib/src/status.rs @@ -0,0 +1,37 @@ +use pyo3::prelude::*; +pub use taskchampion::Status as TCStatus; + +#[pyclass] +#[derive(Clone, Copy)] +pub enum Status { + Pending, + Completed, + Deleted, + Recurring, + /// IMPORTANT: #[pyclass] only supports unit variants + Unknown, +} + +impl From for Status { + fn from(status: TCStatus) -> Self { + return match status { + TCStatus::Pending => Status::Pending, + TCStatus::Completed => Status::Completed, + TCStatus::Deleted => Status::Deleted, + TCStatus::Recurring => Status::Recurring, + _ => Status::Unknown, + }; + } +} + +impl From for TCStatus { + fn from(status: Status) -> Self { + return match status { + Status::Pending => TCStatus::Pending, + Status::Completed => TCStatus::Completed, + Status::Deleted => TCStatus::Deleted, + Status::Recurring => TCStatus::Recurring, + Status::Unknown => TCStatus::Unknown("unknown status".to_string()), + }; + } +} diff --git a/py-lib/src/task.rs b/py-lib/src/task.rs new file mode 100644 index 000000000..662884a94 --- /dev/null +++ b/py-lib/src/task.rs @@ -0,0 +1,10 @@ +use pyo3::prelude::*; +use taskchampion::Task as TCTask; + +#[pyclass] +pub struct Task(pub(crate) TCTask); + +unsafe impl Send for Task {} + +#[pymethods] +impl Task {} diff --git a/py-lib/tests/__init__.py b/py-lib/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/py-lib/tests/test_replica.py b/py-lib/tests/test_replica.py new file mode 100644 index 000000000..a5866bfff --- /dev/null +++ b/py-lib/tests/test_replica.py @@ -0,0 +1,15 @@ +from taskchampion import Replica +import pytest + +from pathlib import Path + + +def test_constructor(tmp_path: Path): + r = Replica(str(tmp_path), True) + + assert r is not None + + +def test_constructor_throws_error_with_missing_database(tmp_path: Path): + with pytest.raises(OSError): + Replica(str(tmp_path), False) diff --git a/py-lib/tests/test_status.py b/py-lib/tests/test_status.py new file mode 100644 index 000000000..e69de29bb diff --git a/py-lib/tests/test_task.py b/py-lib/tests/test_task.py new file mode 100644 index 000000000..e69de29bb From a8cbc6a004c1d46bc2ecd742eb704e9daacc22d7 Mon Sep 17 00:00:00 2001 From: Illya Date: Mon, 6 May 2024 21:01:48 +0300 Subject: [PATCH 03/20] add the rest of the defined tests --- py-lib/src/replica.rs | 12 +++++++++++ py-lib/src/task.rs | 1 + py-lib/tests/test_replica.py | 40 +++++++++++++++++++++++++++++++++++- 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/py-lib/src/replica.rs b/py-lib/src/replica.rs index fa1848f6a..bbb1b375a 100644 --- a/py-lib/src/replica.rs +++ b/py-lib/src/replica.rs @@ -16,6 +16,13 @@ unsafe impl Send for Replica {} #[pymethods] impl Replica { #[new] + /// Instantiates the Replica + /// + /// Args: + /// path (str): path to the directory with the database + /// create_if_missing (bool): create the database if it does not exist + /// Raises: + /// OsError: if database does not exist, and create_if_missing is false pub fn new(path: String, exists: bool) -> PyResult { let storage = SqliteStorage::new(path, exists); // TODO convert this and other match Result into ? for less boilerplate. @@ -24,16 +31,21 @@ impl Replica { Err(e) => Err(PyOSError::new_err(e.to_string())), } } + /// Create a new task + /// The task must not already exist. pub fn new_task(&mut self, status: Status, description: String) { let _ = self.0.new_task(status.into(), description); } + /// Get a list of all uuids for tasks in the replica. pub fn all_task_uuids(&mut self) -> PyResult> { match self.0.all_task_uuids() { Ok(r) => Ok(r.iter().map(|uuid| uuid.to_string()).collect()), Err(e) => Err(PyOSError::new_err(e.to_string())), } } + + /// Get a list of all tasks in the replica. pub fn all_tasks(&mut self) -> PyResult> { match self.0.all_tasks() { Ok(v) => Ok(v diff --git a/py-lib/src/task.rs b/py-lib/src/task.rs index 662884a94..10aa3e0f3 100644 --- a/py-lib/src/task.rs +++ b/py-lib/src/task.rs @@ -1,6 +1,7 @@ use pyo3::prelude::*; use taskchampion::Task as TCTask; +// TODO: actually create a front-facing user class, instead of this data blob #[pyclass] pub struct Task(pub(crate) TCTask); diff --git a/py-lib/tests/test_replica.py b/py-lib/tests/test_replica.py index a5866bfff..2805680f1 100644 --- a/py-lib/tests/test_replica.py +++ b/py-lib/tests/test_replica.py @@ -1,8 +1,15 @@ -from taskchampion import Replica +from taskchampion import Replica, Status import pytest from pathlib import Path +# TODO: instantiate the in-memory replica, this will do for now + + +@pytest.fixture +def new_replica(tmp_path: Path) -> Replica: + return Replica(str(tmp_path), True) + def test_constructor(tmp_path: Path): r = Replica(str(tmp_path), True) @@ -13,3 +20,34 @@ def test_constructor(tmp_path: Path): def test_constructor_throws_error_with_missing_database(tmp_path: Path): with pytest.raises(OSError): Replica(str(tmp_path), False) + + +def test_new_task(new_replica: Replica): + new_replica.new_task(Status.Completed, "This is a desription") + + tasks = new_replica.all_task_uuids() + + assert len(tasks) == 1 + + +def test_all_task_uuids(new_replica: Replica): + new_replica.new_task(Status.Completed, "Task 1") + new_replica.new_task(Status.Completed, "Task 2") + new_replica.new_task(Status.Completed, "Task 3") + + tasks = new_replica.all_task_uuids() + assert len(tasks) == 3 + + +def test_all_tasks(new_replica: Replica): + new_replica.new_task(Status.Completed, "Task 1") + new_replica.new_task(Status.Completed, "Task 2") + new_replica.new_task(Status.Completed, "Task 3") + + tasks = new_replica.all_tasks() + + assert len(tasks) == 3 + keys = tasks.keys() + + for key in keys: + assert tasks[key] != 0 From ba81bdce9de4868595722ebf7797f4eef2b4cc68 Mon Sep 17 00:00:00 2001 From: Illya Date: Tue, 7 May 2024 01:20:15 +0300 Subject: [PATCH 04/20] WIP: add Annotation, many of the immutable task getters --- py-lib/src/lib.rs | 5 +- py-lib/src/task.rs | 175 +++++++++++++++++++++++++++++++++++++- py-lib/tests/test_task.py | 8 ++ 3 files changed, 183 insertions(+), 5 deletions(-) diff --git a/py-lib/src/lib.rs b/py-lib/src/lib.rs index 8cba1005d..3d1cfb9df 100644 --- a/py-lib/src/lib.rs +++ b/py-lib/src/lib.rs @@ -5,11 +5,12 @@ pub mod status; use status::*; pub mod task; use task::*; - +pub mod annotation; +use annotation::*; #[pymodule] fn taskchampion(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - + m.add_class::()?; Ok(()) } diff --git a/py-lib/src/task.rs b/py-lib/src/task.rs index 10aa3e0f3..8557bf37c 100644 --- a/py-lib/src/task.rs +++ b/py-lib/src/task.rs @@ -1,6 +1,6 @@ +use crate::{status::Status, Annotation}; use pyo3::prelude::*; -use taskchampion::Task as TCTask; - +use taskchampion::{Tag as TCTag, Task as TCTask}; // TODO: actually create a front-facing user class, instead of this data blob #[pyclass] pub struct Task(pub(crate) TCTask); @@ -8,4 +8,173 @@ pub struct Task(pub(crate) TCTask); unsafe impl Send for Task {} #[pymethods] -impl Task {} +impl Task { + /// Get a tasks UUID + /// + /// Returns: + /// str: UUID of a task + // TODO: possibly determine if it's possible to turn this from/into python's UUID instead + pub fn get_uuid(&self) -> String { + self.0.get_uuid().to_string() + } + /// Get a task's status + /// Returns: + /// Status: Status subtype + pub fn get_status(&self) -> Status { + self.0.get_status().into() + } + + pub fn get_taskmap(&self) -> PyResult<()> { + unimplemented!() + } + /// Get the entry timestamp for a task + /// + /// Returns: + /// str: RFC3339 timestamp + /// None: No timestamp + // Attempt to convert this into a python datetime later on + pub fn get_entry(&self) -> Option { + self.0.get_entry().map(|timestamp| timestamp.to_rfc3339()) + } + + /// Get the task's priority + /// + /// Returns: + /// str: Task's priority + pub fn get_priority(&self) -> String { + self.0.get_priority().to_string() + } + + /// Get the wait timestamp of the task + /// + /// Returns: + /// str: RFC3339 timestamp + /// None: No timesamp + pub fn get_wait(&self) -> Option { + self.0.get_wait().map(|timestamp| timestamp.to_rfc3339()) + } + /// Check if the task is waiting + /// + /// Returns: + /// bool: if the task is waiting + pub fn is_waiting(&self) -> bool { + self.0.is_waiting() + } + + /// Check if the task is active + /// + /// Returns: + /// bool: if the task is active + pub fn is_active(&self) -> bool { + self.0.is_active() + } + /// Check if the task is blocked + /// + /// Returns: + /// bool: if the task is blocked + pub fn is_blocked(&self) -> bool { + self.0.is_blocked() + } + /// Check if the task is blocking + /// + /// Returns: + /// bool: if the task is blocking + pub fn is_blocking(&self) -> bool { + self.0.is_blocking() + } + /// Check if the task has a tag + /// + /// Returns: + /// bool: if the task has a given tag + pub fn has_tag(&self, tag: &str) -> bool { + if let Ok(tag) = TCTag::try_from(tag) { + self.0.has_tag(&tag) + } else { + false + } + } + + /// Get task tags + /// + /// Returns: + /// list[str]: list of tags + pub fn get_tags(&self) -> Vec { + self.0 + .get_tags() + .into_iter() + .map(|v| v.to_string()) + .collect() + } + /// Get task annotations + /// + /// Returns: + /// list[Annotation]: list of task annotations + pub fn get_annotations(&self) -> Vec { + self.0 + .get_annotations() + .into_iter() + .map(|annotation| Annotation(annotation)) + .collect() + } + + /// Get a task UDA + /// + /// Arguments: + /// namespace (str): argument namespace + /// key (str): argument key + /// + /// Returns: + /// str: UDA value + /// None: Not found + pub fn get_uda(&self, namespace: &str, key: &str) -> Option<&str> { + self.0.get_uda(namespace, key) + } + + // TODO: this signature is ugly and confising, possibly replace this with a struct in the + // actual code + /// get all the task's UDAs + /// + /// Returns: + /// Uh oh, ew? + pub fn get_udas(&self) -> Vec<((&str, &str), &str)> { + self.0.get_udas().collect() + } + /// Get the task modified time + /// + /// Returns: + /// str: RFC3339 modified time + /// None: Not applicable + pub fn get_modified(&self) -> Option { + self.0 + .get_modified() + .map(|timestamp| timestamp.to_rfc3339()) + } + + /// Get the task's due date + /// + /// Returns: + /// str: RFC3339 due date + /// None: No such value + pub fn get_due(&self) -> Option { + self.0.get_due().map(|timestamp| timestamp.to_rfc3339()) + } + /// Get a list of tasks dependencies + /// + /// Returns: + /// list[str]: List of UUIDs of the task depends on + pub fn get_dependencies(&self) -> Vec { + self.0 + .get_dependencies() + .into_iter() + .map(|uuid| uuid.to_string()) + .collect() + } + /// Get the task's property value + /// + /// Returns: + /// str: property value + /// None: no such value + pub fn get_value(&self, property: String) -> Option<&str> { + self.0.get_value(property) + } +} diff --git a/py-lib/tests/test_task.py b/py-lib/tests/test_task.py index e69de29bb..f6a3919eb 100644 --- a/py-lib/tests/test_task.py +++ b/py-lib/tests/test_task.py @@ -0,0 +1,8 @@ +from taskchampion import Task +import pytest + + +@pytest.fixture +def new_task(): + return Task() + pass From 94f525740182e754cc95ab95c42d2148840a21ea Mon Sep 17 00:00:00 2001 From: Illya Date: Tue, 7 May 2024 01:57:51 +0300 Subject: [PATCH 05/20] feat: add tests for the Annotation --- Cargo.lock | 1 + py-lib/Cargo.toml | 1 + py-lib/src/annotation.rs | 36 +++++++++++++++++++++++++++++++++ py-lib/src/lib.rs | 3 +++ py-lib/tests/test_annotation.py | 18 +++++++++++++++++ 5 files changed, 59 insertions(+) create mode 100644 py-lib/src/annotation.rs create mode 100644 py-lib/tests/test_annotation.py diff --git a/Cargo.lock b/Cargo.lock index e58ab5ab7..0fa755c57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1761,6 +1761,7 @@ dependencies = [ name = "taskchampion_python" version = "0.1.0" dependencies = [ + "chrono", "pyo3", "taskchampion", ] diff --git a/py-lib/Cargo.toml b/py-lib/Cargo.toml index f380e17d0..58770de23 100644 --- a/py-lib/Cargo.toml +++ b/py-lib/Cargo.toml @@ -11,6 +11,7 @@ name = "taskchampion" crate-type = ["cdylib"] [dependencies] +chrono.workspace = true pyo3 = "0.20.0" taskchampion = { path = "../taskchampion", version = "0.5.0" } diff --git a/py-lib/src/annotation.rs b/py-lib/src/annotation.rs new file mode 100644 index 000000000..13ff320fa --- /dev/null +++ b/py-lib/src/annotation.rs @@ -0,0 +1,36 @@ +use chrono::DateTime; +use pyo3::prelude::*; +use taskchampion::Annotation as TCAnnotation; +#[pyclass] +/// An annotation for the task +pub struct Annotation(pub(crate) TCAnnotation); + +#[pymethods] +impl Annotation { + #[new] + pub fn new() -> Self { + Annotation(TCAnnotation { + entry: DateTime::default(), + description: String::new(), + }) + } + #[getter] + pub fn entry(&self) -> String { + self.0.entry.to_rfc3339() + } + + #[setter] + pub fn set_entry(&mut self, time: String) { + self.0.entry = DateTime::parse_from_rfc3339(&time).unwrap().into() + } + + #[getter] + pub fn description(&self) -> String { + self.0.description.clone() + } + + #[setter] + pub fn set_description(&mut self, description: String) { + self.0.description = description + } +} diff --git a/py-lib/src/lib.rs b/py-lib/src/lib.rs index 3d1cfb9df..6fb9a48bc 100644 --- a/py-lib/src/lib.rs +++ b/py-lib/src/lib.rs @@ -7,10 +7,13 @@ pub mod task; use task::*; pub mod annotation; use annotation::*; + #[pymodule] fn taskchampion(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + Ok(()) } diff --git a/py-lib/tests/test_annotation.py b/py-lib/tests/test_annotation.py new file mode 100644 index 000000000..019f90d03 --- /dev/null +++ b/py-lib/tests/test_annotation.py @@ -0,0 +1,18 @@ +from taskchampion import Annotation + + +# IDK if this is a good idea, but it seems overkill to have another test to +# test the getter ... while using it for testing +def test_get_set_entry(): + a = Annotation() + a.entry = "2024-05-07T01:35:57+03:00" + + assert a.entry == "2024-05-06T22:35:57+00:00" + + +def test_get_set_description(): + a = Annotation() + + a.description = "This is a basic description" + + assert a.description == "This is a basic description" From 68d24ad72a65c41d9b3109139a08dba699735a90 Mon Sep 17 00:00:00 2001 From: Illya Date: Tue, 7 May 2024 19:47:21 +0300 Subject: [PATCH 06/20] feat: add working_set wrapper, and wrapper tests --- py-lib/src/lib.rs | 3 +++ py-lib/src/replica.rs | 42 +++++++++++++++++++++++++++++++- py-lib/src/working_set.rs | 36 +++++++++++++++++++++++++++ py-lib/tests/test_replica.py | 41 ++++++++++++++++++++----------- py-lib/tests/test_task.py | 6 +++-- py-lib/tests/test_working_set.py | 41 +++++++++++++++++++++++++++++++ 6 files changed, 152 insertions(+), 17 deletions(-) create mode 100644 py-lib/src/working_set.rs create mode 100644 py-lib/tests/test_working_set.py diff --git a/py-lib/src/lib.rs b/py-lib/src/lib.rs index 6fb9a48bc..0a5b26d19 100644 --- a/py-lib/src/lib.rs +++ b/py-lib/src/lib.rs @@ -7,6 +7,8 @@ pub mod task; use task::*; pub mod annotation; use annotation::*; +pub mod working_set; +use working_set::*; #[pymodule] fn taskchampion(_py: Python, m: &PyModule) -> PyResult<()> { @@ -14,6 +16,7 @@ fn taskchampion(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/py-lib/src/replica.rs b/py-lib/src/replica.rs index bbb1b375a..9f3354419 100644 --- a/py-lib/src/replica.rs +++ b/py-lib/src/replica.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use crate::status::Status; -use crate::Task; +use crate::{Task, WorkingSet}; use pyo3::{exceptions::PyOSError, prelude::*}; use taskchampion::storage::SqliteStorage; use taskchampion::Replica as TCReplica; @@ -25,6 +25,7 @@ impl Replica { /// OsError: if database does not exist, and create_if_missing is false pub fn new(path: String, exists: bool) -> PyResult { let storage = SqliteStorage::new(path, exists); + // TODO convert this and other match Result into ? for less boilerplate. match storage { Ok(v) => Ok(Replica(TCReplica::new(Box::new(v)))), @@ -55,4 +56,43 @@ impl Replica { Err(e) => Err(PyOSError::new_err(e.to_string())), } } + + pub fn update_task(&self) { + todo!() + } + + pub fn working_set(&mut self) -> PyResult { + match self.0.working_set() { + Ok(ws) => Ok(WorkingSet(ws)), + Err(err) => Err(PyOSError::new_err(err.to_string())), + } + } + pub fn dependency_map(&self, _force: bool) { + todo!() + } + + pub fn get_task(&mut self, _uuid: String) -> PyResult> { + todo!() + } + + pub fn import_task_with_uuid(&self, _uuid: String) -> PyResult { + todo!() + } + pub fn sync(&self, _avoid_snapshots: bool) { + todo!() + } + + pub fn rebuild_working_set(&self, _renumber: bool) -> PyResult<()> { + todo!() + } + pub fn add_undo_point(&mut self, _force: bool) -> PyResult<()> { + todo!() + } + pub fn num_local_operations(&mut self) -> PyResult { + todo!() + } + + pub fn num_undo_points(&self) -> PyResult { + todo!() + } } diff --git a/py-lib/src/working_set.rs b/py-lib/src/working_set.rs new file mode 100644 index 000000000..720be0e8c --- /dev/null +++ b/py-lib/src/working_set.rs @@ -0,0 +1,36 @@ +use pyo3::prelude::*; +use taskchampion::Uuid; +use taskchampion::WorkingSet as TCWorkingSet; +// TODO: convert working set into python's iterable type +#[pyclass] +pub struct WorkingSet(pub(crate) TCWorkingSet); + +#[pymethods] +impl WorkingSet { + pub fn __len__(&self) -> usize { + self.0.len() + } + + pub fn largest_index(&self) -> usize { + self.0.largest_index() + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn by_index(&self, index: usize) -> Option { + self.0.by_index(index).map(|uuid| uuid.into()) + } + + pub fn by_uuid(&self, uuid: String) -> Option { + // TODO I don't like the conversion, should use try-expect or something else as an input + self.0.by_uuid(Uuid::parse_str(&uuid).unwrap()) + } + + fn __iter__(_slf: PyRef<'_, Self>) -> PyResult> { + todo!("Figure way to propertly implement iterator for python") + // Usability-wise we want it to hold the reference to the iterator, so that + // with each iteration the state persists. + } +} diff --git a/py-lib/tests/test_replica.py b/py-lib/tests/test_replica.py index 2805680f1..62f584420 100644 --- a/py-lib/tests/test_replica.py +++ b/py-lib/tests/test_replica.py @@ -7,10 +7,18 @@ @pytest.fixture -def new_replica(tmp_path: Path) -> Replica: +def empty_replica(tmp_path: Path) -> Replica: return Replica(str(tmp_path), True) +@pytest.fixture +def replica_with_tasks(empty_replica: Replica): + empty_replica.new_task(Status.Pending, "Task 1") + empty_replica.new_task(Status.Pending, "Task 2") + + return empty_replica + + def test_constructor(tmp_path: Path): r = Replica(str(tmp_path), True) @@ -22,32 +30,37 @@ def test_constructor_throws_error_with_missing_database(tmp_path: Path): Replica(str(tmp_path), False) -def test_new_task(new_replica: Replica): - new_replica.new_task(Status.Completed, "This is a desription") +def test_new_task(empty_replica: Replica): + empty_replica.new_task(Status.Completed, "This is a desription") - tasks = new_replica.all_task_uuids() + tasks = empty_replica.all_task_uuids() assert len(tasks) == 1 -def test_all_task_uuids(new_replica: Replica): - new_replica.new_task(Status.Completed, "Task 1") - new_replica.new_task(Status.Completed, "Task 2") - new_replica.new_task(Status.Completed, "Task 3") +def test_all_task_uuids(empty_replica: Replica): + empty_replica.new_task(Status.Completed, "Task 1") + empty_replica.new_task(Status.Completed, "Task 2") + empty_replica.new_task(Status.Completed, "Task 3") - tasks = new_replica.all_task_uuids() + tasks = empty_replica.all_task_uuids() assert len(tasks) == 3 -def test_all_tasks(new_replica: Replica): - new_replica.new_task(Status.Completed, "Task 1") - new_replica.new_task(Status.Completed, "Task 2") - new_replica.new_task(Status.Completed, "Task 3") +def test_all_tasks(empty_replica: Replica): + empty_replica.new_task(Status.Completed, "Task 1") + empty_replica.new_task(Status.Completed, "Task 2") + empty_replica.new_task(Status.Completed, "Task 3") - tasks = new_replica.all_tasks() + tasks = empty_replica.all_tasks() assert len(tasks) == 3 keys = tasks.keys() for key in keys: assert tasks[key] != 0 + + +def test_working_set(replica_with_tasks: Replica): + ws = replica_with_tasks.working_set() + pass diff --git a/py-lib/tests/test_task.py b/py-lib/tests/test_task.py index f6a3919eb..855d6610f 100644 --- a/py-lib/tests/test_task.py +++ b/py-lib/tests/test_task.py @@ -1,8 +1,10 @@ -from taskchampion import Task +from taskchampion import Task, Replica import pytest @pytest.fixture -def new_task(): +def new_task(tmp_path): + r = Replica(str(tmp_path), True) + r.get return Task() pass diff --git a/py-lib/tests/test_working_set.py b/py-lib/tests/test_working_set.py new file mode 100644 index 000000000..d085f8728 --- /dev/null +++ b/py-lib/tests/test_working_set.py @@ -0,0 +1,41 @@ +from taskchampion import Replica, Status, WorkingSet +from pathlib import Path +import pytest + + +@pytest.fixture +def working_set(tmp_path: Path): + r = Replica(str(tmp_path), True) + r.new_task(Status.Pending, "Task 1") + r.new_task(Status.Pending, "Task 2") + + return r.working_set() + + +def test_len(working_set: WorkingSet): + assert len(working_set) == 2 + + +def test_largest_index(working_set: WorkingSet): + assert working_set.largest_index() == 2 + + +def test_is_empty(working_set: WorkingSet): + assert not working_set.is_empty() + + +def test_by_index(working_set: WorkingSet): + assert working_set.by_index(1) is not None + + +@pytest.mark.skip() +def test_iter(working_set: WorkingSet): + assert iter(working_set) + + +@pytest.mark.skip() +def test_next(working_set: WorkingSet): + assert next(working_set)[0] == 1 + assert next(working_set)[0] == 2 + with pytest.raises(OSError): + next(working_set) From 8555ff41f715dce8e0bf10a3ec854be109d3f097 Mon Sep 17 00:00:00 2001 From: Illya Date: Tue, 7 May 2024 21:23:54 +0300 Subject: [PATCH 07/20] docs: fix spelling error --- taskchampion/src/depmap.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/taskchampion/src/depmap.rs b/taskchampion/src/depmap.rs index fe4d5df6f..fd384bc11 100644 --- a/taskchampion/src/depmap.rs +++ b/taskchampion/src/depmap.rs @@ -6,7 +6,7 @@ use uuid::Uuid; /// typically calculated once and re-used. #[derive(Debug, PartialEq, Eq)] pub struct DependencyMap { - /// Edges of the dependency graph. If (a, b) is in this array, then task a depends on tsak b. + /// Edges of the dependency graph. If (a, b) is in this array, then task a depends on task b. edges: Vec<(Uuid, Uuid)>, } From e7d6dd086254859ac04ebdcbbd5e5289715c51b0 Mon Sep 17 00:00:00 2001 From: Illya Date: Tue, 7 May 2024 22:18:18 +0300 Subject: [PATCH 08/20] feat: Replica mappings, and tests --- py-lib/src/dependency_map.rs | 0 py-lib/src/replica.rs | 61 +++++++++++++++++++++++++----------- py-lib/tests/test_replica.py | 42 ++++++++++++++++++++++++- 3 files changed, 84 insertions(+), 19 deletions(-) create mode 100644 py-lib/src/dependency_map.rs diff --git a/py-lib/src/dependency_map.rs b/py-lib/src/dependency_map.rs new file mode 100644 index 000000000..e69de29bb diff --git a/py-lib/src/replica.rs b/py-lib/src/replica.rs index 9f3354419..5791942c7 100644 --- a/py-lib/src/replica.rs +++ b/py-lib/src/replica.rs @@ -4,7 +4,7 @@ use crate::status::Status; use crate::{Task, WorkingSet}; use pyo3::{exceptions::PyOSError, prelude::*}; use taskchampion::storage::SqliteStorage; -use taskchampion::Replica as TCReplica; +use taskchampion::{Replica as TCReplica, Uuid}; #[pyclass] /// A replica represents an instance of a user's task data, providing an easy interface @@ -12,7 +12,6 @@ use taskchampion::Replica as TCReplica; pub struct Replica(TCReplica); unsafe impl Send for Replica {} - #[pymethods] impl Replica { #[new] @@ -57,8 +56,17 @@ impl Replica { } } - pub fn update_task(&self) { - todo!() + pub fn update_task( + &mut self, + uuid: String, + property: String, + value: Option, + ) -> PyResult> { + let uuid = Uuid::parse_str(&uuid).unwrap(); + match self.0.update_task(uuid, property, value) { + Ok(res) => Ok(res), + Err(e) => Err(PyOSError::new_err(e.to_string())), + } } pub fn working_set(&mut self) -> PyResult { @@ -67,32 +75,49 @@ impl Replica { Err(err) => Err(PyOSError::new_err(err.to_string())), } } - pub fn dependency_map(&self, _force: bool) { - todo!() - } - pub fn get_task(&mut self, _uuid: String) -> PyResult> { - todo!() + // pub fn dependency_map(&self, force: bool) { + // self.0.dependency_map(force) + // } + + pub fn get_task(&mut self, uuid: String) -> PyResult> { + // TODO: it should be possible to wrap this into a HOF that does two maps automatically + // thus reducing boilerplate + self.0 + .get_task(Uuid::parse_str(&uuid).unwrap()) + .map(|opt| opt.map(|t| Task(t))) + .map_err(|e| PyOSError::new_err(e.to_string())) } - pub fn import_task_with_uuid(&self, _uuid: String) -> PyResult { - todo!() + pub fn import_task_with_uuid(&mut self, uuid: String) -> PyResult { + self.0 + .import_task_with_uuid(Uuid::parse_str(&uuid).unwrap()) + .map(|task| Task(task)) + .map_err(|err| PyOSError::new_err(err.to_string())) } pub fn sync(&self, _avoid_snapshots: bool) { todo!() } - pub fn rebuild_working_set(&self, _renumber: bool) -> PyResult<()> { - todo!() + pub fn rebuild_working_set(&mut self, renumber: bool) -> PyResult<()> { + self.0 + .rebuild_working_set(renumber) + .map_err(|err| PyOSError::new_err(err.to_string())) } - pub fn add_undo_point(&mut self, _force: bool) -> PyResult<()> { - todo!() + pub fn add_undo_point(&mut self, force: bool) -> PyResult<()> { + self.0 + .add_undo_point(force) + .map_err(|err| PyOSError::new_err(err.to_string())) } pub fn num_local_operations(&mut self) -> PyResult { - todo!() + self.0 + .num_local_operations() + .map_err(|err| PyOSError::new_err(err.to_string())) } - pub fn num_undo_points(&self) -> PyResult { - todo!() + pub fn num_undo_points(&mut self) -> PyResult { + self.0 + .num_local_operations() + .map_err(|err| PyOSError::new_err(err.to_string())) } } diff --git a/py-lib/tests/test_replica.py b/py-lib/tests/test_replica.py index 62f584420..9910c840c 100644 --- a/py-lib/tests/test_replica.py +++ b/py-lib/tests/test_replica.py @@ -3,6 +3,8 @@ from pathlib import Path +import uuid + # TODO: instantiate the in-memory replica, this will do for now @@ -63,4 +65,42 @@ def test_all_tasks(empty_replica: Replica): def test_working_set(replica_with_tasks: Replica): ws = replica_with_tasks.working_set() - pass + assert ws is not None + + +# TODO: create testable and inspectable WorkingSet + + +def test_get_task(replica_with_tasks: Replica): + uuid = replica_with_tasks.all_task_uuids()[0] + + task = replica_with_tasks.get_task(uuid) + + assert task is not None + + +def test_rebuild_working_set(replica_with_tasks: Replica): + replica_with_tasks.rebuild_working_set(False) + + +def test_add_undo_point(replica_with_tasks: Replica): + replica_with_tasks.add_undo_point(False) + + +def test_num_local_operations(replica_with_tasks: Replica): + assert replica_with_tasks.num_local_operations() == 10 + replica_with_tasks.new_task(Status.Pending, "New task 3") + assert replica_with_tasks.num_local_operations() == 15 + + +def test_num_undo_points(replica_with_tasks: Replica): + assert replica_with_tasks.num_undo_points() == 10 + replica_with_tasks.add_undo_point(True) + + assert replica_with_tasks.num_undo_points() == 11 + + +def import_task_with_uuid(replica_with_tasks: Replica): + # TODO: figure out failure reason + replica_with_tasks.import_task_with_uuid(str(uuid.uuid4())) + assert len(replica_with_tasks.all_task_uuids()) == 3 From 0d5ed7a54475a0ee0c2c2bc00415df47e4c5199e Mon Sep 17 00:00:00 2001 From: Illya Date: Wed, 8 May 2024 15:18:42 +0300 Subject: [PATCH 09/20] feat: finalize replica tests --- py-lib/tests/test_replica.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/py-lib/tests/test_replica.py b/py-lib/tests/test_replica.py index 9910c840c..9aedf627b 100644 --- a/py-lib/tests/test_replica.py +++ b/py-lib/tests/test_replica.py @@ -95,9 +95,10 @@ def test_num_local_operations(replica_with_tasks: Replica): def test_num_undo_points(replica_with_tasks: Replica): assert replica_with_tasks.num_undo_points() == 10 + replica_with_tasks.new_task(Status.Pending, "Another task") replica_with_tasks.add_undo_point(True) - assert replica_with_tasks.num_undo_points() == 11 + assert replica_with_tasks.num_undo_points() == 15 def import_task_with_uuid(replica_with_tasks: Replica): From 0e62f0a41c117865814c242e2732c7f3b8782391 Mon Sep 17 00:00:00 2001 From: Illya Date: Fri, 10 May 2024 13:30:14 +0300 Subject: [PATCH 10/20] feat: create storage classes --- Cargo.lock | 2 ++ py-lib/Cargo.toml | 3 ++- py-lib/notes.md | 1 + py-lib/src/lib.rs | 7 +++++++ py-lib/src/replica.rs | 26 ++++++++++++++++---------- py-lib/src/storage.rs | 35 +++++++++++++++++++++++++++++++++++ py-lib/src/tag.rs | 18 ++++++++++++++++++ py-lib/src/task.rs | 18 +++++------------- py-lib/tests/test_tag.py | 1 + 9 files changed, 87 insertions(+), 24 deletions(-) create mode 100644 py-lib/src/storage.rs create mode 100644 py-lib/src/tag.rs create mode 100644 py-lib/tests/test_tag.py diff --git a/Cargo.lock b/Cargo.lock index 0fa755c57..4ff9f39a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1177,6 +1177,7 @@ version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" dependencies = [ + "anyhow", "cfg-if", "indoc", "libc", @@ -1761,6 +1762,7 @@ dependencies = [ name = "taskchampion_python" version = "0.1.0" dependencies = [ + "anyhow", "chrono", "pyo3", "taskchampion", diff --git a/py-lib/Cargo.toml b/py-lib/Cargo.toml index 58770de23..5fbb70264 100644 --- a/py-lib/Cargo.toml +++ b/py-lib/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["cdylib"] [dependencies] chrono.workspace = true -pyo3 = "0.20.0" +pyo3 = { verison = "0.20.0", features = ["anyhow"] } taskchampion = { path = "../taskchampion", version = "0.5.0" } +anyhow = "*" diff --git a/py-lib/notes.md b/py-lib/notes.md index c97ad7ba4..fede19123 100644 --- a/py-lib/notes.md +++ b/py-lib/notes.md @@ -1,4 +1,5 @@ # Notes while developing the project - renamed the package to taskchampion, instead of py_lib, so the python imports work nicely +- Implementing 2nd python class that is mutable and that has it's own mutable methods should be easier than trying to use single object that can have both states. diff --git a/py-lib/src/lib.rs b/py-lib/src/lib.rs index 0a5b26d19..7fe17fdcf 100644 --- a/py-lib/src/lib.rs +++ b/py-lib/src/lib.rs @@ -9,6 +9,10 @@ pub mod annotation; use annotation::*; pub mod working_set; use working_set::*; +pub mod tag; +use tag::*; +pub mod storage; +use storage::*; #[pymodule] fn taskchampion(_py: Python, m: &PyModule) -> PyResult<()> { @@ -17,6 +21,9 @@ fn taskchampion(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/py-lib/src/replica.rs b/py-lib/src/replica.rs index 5791942c7..342822ba5 100644 --- a/py-lib/src/replica.rs +++ b/py-lib/src/replica.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::status::Status; use crate::{Task, WorkingSet}; use pyo3::{exceptions::PyOSError, prelude::*}; -use taskchampion::storage::SqliteStorage; +use taskchampion::storage::{InMemoryStorage, SqliteStorage}; use taskchampion::{Replica as TCReplica, Uuid}; #[pyclass] @@ -22,19 +22,25 @@ impl Replica { /// create_if_missing (bool): create the database if it does not exist /// Raises: /// OsError: if database does not exist, and create_if_missing is false - pub fn new(path: String, exists: bool) -> PyResult { - let storage = SqliteStorage::new(path, exists); + pub fn new(path: String, create_if_missing: bool) -> anyhow::Result { + let storage = SqliteStorage::new(path, create_if_missing)?; - // TODO convert this and other match Result into ? for less boilerplate. - match storage { - Ok(v) => Ok(Replica(TCReplica::new(Box::new(v)))), - Err(e) => Err(PyOSError::new_err(e.to_string())), - } + Ok(Replica(TCReplica::new(Box::new(storage)))) + } + + #[staticmethod] + pub fn new_inmemory() -> Self { + let storage = InMemoryStorage::new(); + + Replica(TCReplica::new(Box::new(storage))) } /// Create a new task /// The task must not already exist. - pub fn new_task(&mut self, status: Status, description: String) { - let _ = self.0.new_task(status.into(), description); + pub fn new_task(&mut self, status: Status, description: String) -> PyResult { + self.0 + .new_task(status.into(), description) + .map(|t| Task(t)) + .map_err(|e| PyOSError::new_err(e.to_string())) } /// Get a list of all uuids for tasks in the replica. diff --git a/py-lib/src/storage.rs b/py-lib/src/storage.rs new file mode 100644 index 000000000..7c9fd7d7e --- /dev/null +++ b/py-lib/src/storage.rs @@ -0,0 +1,35 @@ +use pyo3::prelude::*; +use taskchampion::storage::{ + InMemoryStorage as TCInMemoryStorage, SqliteStorage as TCSqliteStorage, +}; +// TODO: actually make the storage usable and extensible, rn it just exists /shrug +pub trait Storage {} +#[pyclass] +pub struct InMemoryStorage(TCInMemoryStorage); + +#[pymethods] +impl InMemoryStorage { + #[new] + pub fn new() -> InMemoryStorage { + InMemoryStorage(TCInMemoryStorage::new()) + } +} + +impl Storage for InMemoryStorage {} + +#[pyclass] +pub struct SqliteStorage(TCSqliteStorage); + +#[pymethods] +impl SqliteStorage { + #[new] + pub fn new(path: String, create_if_missing: bool) -> anyhow::Result { + // TODO: kinda ugly, prettify; + Ok(SqliteStorage(TCSqliteStorage::new( + path, + create_if_missing, + )?)) + } +} + +impl Storage for SqliteStorage {} diff --git a/py-lib/src/tag.rs b/py-lib/src/tag.rs new file mode 100644 index 000000000..e4479957a --- /dev/null +++ b/py-lib/src/tag.rs @@ -0,0 +1,18 @@ +use pyo3::prelude::*; +use taskchampion::Tag as TCTag; + +/// TODO: following the api there currently is no way to construct the task, not sure if this is +/// correct +#[pyclass] +pub struct Tag(pub(crate) TCTag); + +#[pymethods] +impl Tag { + pub fn is_synthetic(&self) -> bool { + self.0.is_synthetic() + } + + pub fn is_user(&self) -> bool { + self.0.is_user() + } +} diff --git a/py-lib/src/task.rs b/py-lib/src/task.rs index 8557bf37c..7e60eef4e 100644 --- a/py-lib/src/task.rs +++ b/py-lib/src/task.rs @@ -1,4 +1,4 @@ -use crate::{status::Status, Annotation}; +use crate::{status::Status, Annotation, Tag}; use pyo3::prelude::*; use taskchampion::{Tag as TCTag, Task as TCTask}; // TODO: actually create a front-facing user class, instead of this data blob @@ -86,24 +86,16 @@ impl Task { /// /// Returns: /// bool: if the task has a given tag - pub fn has_tag(&self, tag: &str) -> bool { - if let Ok(tag) = TCTag::try_from(tag) { - self.0.has_tag(&tag) - } else { - false - } + pub fn has_tag(&self, tag: &Tag) -> bool { + self.0.has_tag(&tag.0) } /// Get task tags /// /// Returns: /// list[str]: list of tags - pub fn get_tags(&self) -> Vec { - self.0 - .get_tags() - .into_iter() - .map(|v| v.to_string()) - .collect() + pub fn get_tags(&self) -> Vec { + self.0.get_tags().into_iter().map(|v| Tag(v)).collect() } /// Get task annotations /// diff --git a/py-lib/tests/test_tag.py b/py-lib/tests/test_tag.py new file mode 100644 index 000000000..6a2ad9303 --- /dev/null +++ b/py-lib/tests/test_tag.py @@ -0,0 +1 @@ +# TODO: test the darn thing From 68e890cd2512346cf64174ecc71bbaed54f199e5 Mon Sep 17 00:00:00 2001 From: Illya Date: Fri, 10 May 2024 13:42:25 +0300 Subject: [PATCH 11/20] feat: add DependencyMap class and methods --- py-lib/src/dependency_map.rs | 20 ++++++++++++++++++++ py-lib/src/lib.rs | 3 +++ 2 files changed, 23 insertions(+) diff --git a/py-lib/src/dependency_map.rs b/py-lib/src/dependency_map.rs index e69de29bb..3bda0e711 100644 --- a/py-lib/src/dependency_map.rs +++ b/py-lib/src/dependency_map.rs @@ -0,0 +1,20 @@ +use pyo3::prelude::*; +use taskchampion::{DependencyMap as TCDependencyMap, Uuid}; + +#[pyclass] +pub struct DependencyMap(TCDependencyMap); + +#[pymethods] +impl DependencyMap { + // TODO: possibly optimize this later, if possible + pub fn dependencies(&self, dep_of: String) -> Vec { + let uuid = Uuid::parse_str(&dep_of).unwrap(); + self.0.dependencies(uuid).map(|uuid| uuid.into()).collect() + } + + pub fn dependents(&self, dep_on: String) -> Vec { + let uuid = Uuid::parse_str(&dep_on).unwrap(); + + self.0.dependencies(uuid).map(|uuid| uuid.into()).collect() + } +} diff --git a/py-lib/src/lib.rs b/py-lib/src/lib.rs index 7fe17fdcf..9029841f0 100644 --- a/py-lib/src/lib.rs +++ b/py-lib/src/lib.rs @@ -13,6 +13,8 @@ pub mod tag; use tag::*; pub mod storage; use storage::*; +pub mod dependency_map; +use dependency_map::*; #[pymodule] fn taskchampion(_py: Python, m: &PyModule) -> PyResult<()> { @@ -24,6 +26,7 @@ fn taskchampion(_py: Python, m: &PyModule) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; Ok(()) } From 598bff156d0014fbd5f24a1bea6e2db37bc2a708 Mon Sep 17 00:00:00 2001 From: Illya Date: Fri, 10 May 2024 15:27:02 +0300 Subject: [PATCH 12/20] feat: add replica dependency_map method --- py-lib/src/dependency_map.rs | 2 +- py-lib/src/replica.rs | 19 +++++++++++++++---- py-lib/src/storage.rs | 3 +++ py-lib/tests/test_replica.py | 9 +++++++-- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/py-lib/src/dependency_map.rs b/py-lib/src/dependency_map.rs index 3bda0e711..940d71ed6 100644 --- a/py-lib/src/dependency_map.rs +++ b/py-lib/src/dependency_map.rs @@ -2,7 +2,7 @@ use pyo3::prelude::*; use taskchampion::{DependencyMap as TCDependencyMap, Uuid}; #[pyclass] -pub struct DependencyMap(TCDependencyMap); +pub struct DependencyMap(pub(crate) TCDependencyMap); #[pymethods] impl DependencyMap { diff --git a/py-lib/src/replica.rs b/py-lib/src/replica.rs index 342822ba5..c8a4409b6 100644 --- a/py-lib/src/replica.rs +++ b/py-lib/src/replica.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; +use std::rc::Rc; use crate::status::Status; -use crate::{Task, WorkingSet}; +use crate::{DependencyMap, Task, WorkingSet}; use pyo3::{exceptions::PyOSError, prelude::*}; use taskchampion::storage::{InMemoryStorage, SqliteStorage}; use taskchampion::{Replica as TCReplica, Uuid}; @@ -82,9 +83,19 @@ impl Replica { } } - // pub fn dependency_map(&self, force: bool) { - // self.0.dependency_map(force) - // } + pub fn dependency_map(&mut self, force: bool) -> anyhow::Result { + // TODO: kinda spaghetti here, it will do for now + let s = self + .0 + .dependency_map(force) + .map(|rc| { + // TODO: better error handling here + Rc::into_inner(rc).unwrap() + }) + .map(|dm| DependencyMap(dm))?; + + Ok(s) + } pub fn get_task(&mut self, uuid: String) -> PyResult> { // TODO: it should be possible to wrap this into a HOF that does two maps automatically diff --git a/py-lib/src/storage.rs b/py-lib/src/storage.rs index 7c9fd7d7e..b7c1ae96e 100644 --- a/py-lib/src/storage.rs +++ b/py-lib/src/storage.rs @@ -4,6 +4,8 @@ use taskchampion::storage::{ }; // TODO: actually make the storage usable and extensible, rn it just exists /shrug pub trait Storage {} + +#[allow(dead_code)] #[pyclass] pub struct InMemoryStorage(TCInMemoryStorage); @@ -17,6 +19,7 @@ impl InMemoryStorage { impl Storage for InMemoryStorage {} +#[allow(dead_code)] #[pyclass] pub struct SqliteStorage(TCSqliteStorage); diff --git a/py-lib/tests/test_replica.py b/py-lib/tests/test_replica.py index 9aedf627b..f65ec2ab3 100644 --- a/py-lib/tests/test_replica.py +++ b/py-lib/tests/test_replica.py @@ -28,7 +28,7 @@ def test_constructor(tmp_path: Path): def test_constructor_throws_error_with_missing_database(tmp_path: Path): - with pytest.raises(OSError): + with pytest.raises(RuntimeError): Replica(str(tmp_path), False) @@ -101,7 +101,12 @@ def test_num_undo_points(replica_with_tasks: Replica): assert replica_with_tasks.num_undo_points() == 15 -def import_task_with_uuid(replica_with_tasks: Replica): +def test_import_task_with_uuid(replica_with_tasks: Replica): # TODO: figure out failure reason replica_with_tasks.import_task_with_uuid(str(uuid.uuid4())) assert len(replica_with_tasks.all_task_uuids()) == 3 + + +@pytest.mark.skip("Skipping as gotta actually polish it") +def test_dependency_map(replica_with_tasks: Replica): + assert replica_with_tasks.dependency_map(False) is not None From 9aa36b3b43a5eb3193bcbd919bcce660318e2929 Mon Sep 17 00:00:00 2001 From: Illya Date: Fri, 10 May 2024 17:57:54 +0300 Subject: [PATCH 13/20] feat: use anyhow to handle error conversions --- py-lib/notes.md | 2 +- py-lib/src/replica.rs | 94 +++++++++++++++--------------------- py-lib/src/task.rs | 2 +- py-lib/tests/test_replica.py | 15 +++++- 4 files changed, 54 insertions(+), 59 deletions(-) diff --git a/py-lib/notes.md b/py-lib/notes.md index fede19123..3f4c9d851 100644 --- a/py-lib/notes.md +++ b/py-lib/notes.md @@ -2,4 +2,4 @@ - renamed the package to taskchampion, instead of py_lib, so the python imports work nicely - Implementing 2nd python class that is mutable and that has it's own mutable methods should be easier than trying to use single object that can have both states. - +- Scratch that, sanely wrapping TaskMut with a lifetime param is not possible w/ pyo3, python just cannot handle lifetimes. So what to do with this? Should I go unsafe route or if there is a gimmick I could exploit, akin to C-lib? diff --git a/py-lib/src/replica.rs b/py-lib/src/replica.rs index c8a4409b6..10202347b 100644 --- a/py-lib/src/replica.rs +++ b/py-lib/src/replica.rs @@ -22,7 +22,7 @@ impl Replica { /// path (str): path to the directory with the database /// create_if_missing (bool): create the database if it does not exist /// Raises: - /// OsError: if database does not exist, and create_if_missing is false + /// RuntimeError: if database does not exist, and create_if_missing is false pub fn new(path: String, create_if_missing: bool) -> anyhow::Result { let storage = SqliteStorage::new(path, create_if_missing)?; @@ -37,30 +37,29 @@ impl Replica { } /// Create a new task /// The task must not already exist. - pub fn new_task(&mut self, status: Status, description: String) -> PyResult { - self.0 + pub fn new_task(&mut self, status: Status, description: String) -> anyhow::Result { + Ok(self + .0 .new_task(status.into(), description) - .map(|t| Task(t)) - .map_err(|e| PyOSError::new_err(e.to_string())) + .map(|t| Task(t))?) } /// Get a list of all uuids for tasks in the replica. - pub fn all_task_uuids(&mut self) -> PyResult> { - match self.0.all_task_uuids() { - Ok(r) => Ok(r.iter().map(|uuid| uuid.to_string()).collect()), - Err(e) => Err(PyOSError::new_err(e.to_string())), - } + pub fn all_task_uuids(&mut self) -> anyhow::Result> { + Ok(self + .0 + .all_task_uuids() + .map(|v| v.iter().map(|item| item.to_string()).collect())?) } /// Get a list of all tasks in the replica. - pub fn all_tasks(&mut self) -> PyResult> { - match self.0.all_tasks() { - Ok(v) => Ok(v - .into_iter() - .map(|(key, value)| (key.to_string(), Task(value))) - .collect()), - Err(e) => Err(PyOSError::new_err(e.to_string())), - } + pub fn all_tasks(&mut self) -> anyhow::Result> { + Ok(self + .0 + .all_tasks()? + .into_iter() + .map(|(key, value)| (key.to_string(), Task(value))) + .collect()) } pub fn update_task( @@ -68,19 +67,14 @@ impl Replica { uuid: String, property: String, value: Option, - ) -> PyResult> { - let uuid = Uuid::parse_str(&uuid).unwrap(); - match self.0.update_task(uuid, property, value) { - Ok(res) => Ok(res), - Err(e) => Err(PyOSError::new_err(e.to_string())), - } + ) -> anyhow::Result> { + let uuid = Uuid::parse_str(&uuid)?; + + Ok(self.0.update_task(uuid, property, value)?) } - pub fn working_set(&mut self) -> PyResult { - match self.0.working_set() { - Ok(ws) => Ok(WorkingSet(ws)), - Err(err) => Err(PyOSError::new_err(err.to_string())), - } + pub fn working_set(&mut self) -> anyhow::Result { + Ok(self.0.working_set().map(|ws| WorkingSet(ws))?) } pub fn dependency_map(&mut self, force: bool) -> anyhow::Result { @@ -97,44 +91,34 @@ impl Replica { Ok(s) } - pub fn get_task(&mut self, uuid: String) -> PyResult> { - // TODO: it should be possible to wrap this into a HOF that does two maps automatically - // thus reducing boilerplate - self.0 + pub fn get_task(&mut self, uuid: String) -> anyhow::Result> { + Ok(self + .0 .get_task(Uuid::parse_str(&uuid).unwrap()) - .map(|opt| opt.map(|t| Task(t))) - .map_err(|e| PyOSError::new_err(e.to_string())) + .map(|opt| opt.map(|t| Task(t)))?) } - pub fn import_task_with_uuid(&mut self, uuid: String) -> PyResult { - self.0 + pub fn import_task_with_uuid(&mut self, uuid: String) -> anyhow::Result { + Ok(self + .0 .import_task_with_uuid(Uuid::parse_str(&uuid).unwrap()) - .map(|task| Task(task)) - .map_err(|err| PyOSError::new_err(err.to_string())) + .map(|task| Task(task))?) } pub fn sync(&self, _avoid_snapshots: bool) { todo!() } - pub fn rebuild_working_set(&mut self, renumber: bool) -> PyResult<()> { - self.0 - .rebuild_working_set(renumber) - .map_err(|err| PyOSError::new_err(err.to_string())) + pub fn rebuild_working_set(&mut self, renumber: bool) -> anyhow::Result<()> { + Ok(self.0.rebuild_working_set(renumber)?) } - pub fn add_undo_point(&mut self, force: bool) -> PyResult<()> { - self.0 - .add_undo_point(force) - .map_err(|err| PyOSError::new_err(err.to_string())) + pub fn add_undo_point(&mut self, force: bool) -> anyhow::Result<()> { + Ok(self.0.add_undo_point(force)?) } - pub fn num_local_operations(&mut self) -> PyResult { - self.0 - .num_local_operations() - .map_err(|err| PyOSError::new_err(err.to_string())) + pub fn num_local_operations(&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(|err| PyOSError::new_err(err.to_string())) + pub fn num_undo_points(&mut self) -> anyhow::Result { + Ok(self.0.num_local_operations()?) } } diff --git a/py-lib/src/task.rs b/py-lib/src/task.rs index 7e60eef4e..fcb654720 100644 --- a/py-lib/src/task.rs +++ b/py-lib/src/task.rs @@ -1,6 +1,6 @@ use crate::{status::Status, Annotation, Tag}; use pyo3::prelude::*; -use taskchampion::{Tag as TCTag, Task as TCTask}; +use taskchampion::{Task as TCTask, TaskMut as TCTaskMut}; // TODO: actually create a front-facing user class, instead of this data blob #[pyclass] pub struct Task(pub(crate) TCTask); diff --git a/py-lib/tests/test_replica.py b/py-lib/tests/test_replica.py index f65ec2ab3..5e5cd4c56 100644 --- a/py-lib/tests/test_replica.py +++ b/py-lib/tests/test_replica.py @@ -1,4 +1,4 @@ -from taskchampion import Replica, Status +from taskchampion import Replica, Status, WorkingSet import pytest from pathlib import Path @@ -64,7 +64,8 @@ def test_all_tasks(empty_replica: Replica): def test_working_set(replica_with_tasks: Replica): - ws = replica_with_tasks.working_set() + ws: WorkingSet = replica_with_tasks.working_set() + assert ws is not None @@ -80,6 +81,7 @@ def test_get_task(replica_with_tasks: Replica): def test_rebuild_working_set(replica_with_tasks: Replica): + # TODO actually test this replica_with_tasks.rebuild_working_set(False) @@ -110,3 +112,12 @@ def test_import_task_with_uuid(replica_with_tasks: Replica): @pytest.mark.skip("Skipping as gotta actually polish it") def test_dependency_map(replica_with_tasks: Replica): assert replica_with_tasks.dependency_map(False) is not None + + +def test_update_task(replica_with_tasks: Replica): + task_uuid = replica_with_tasks.all_task_uuids()[0] + + res = replica_with_tasks.update_task( + task_uuid, "description", "This text here") + assert res["description"] == "This text here" + assert res["status"] == "pending" From 9b2dae202a18d986ad6e86b1ccf17de8862cd1db Mon Sep 17 00:00:00 2001 From: Illya Date: Fri, 10 May 2024 22:15:14 +0300 Subject: [PATCH 14/20] feat: test most of the Task class --- py-lib/Cargo.toml | 2 +- py-lib/notes.md | 2 +- py-lib/src/replica.rs | 2 +- py-lib/src/tag.rs | 6 +- py-lib/src/task.rs | 10 ++- py-lib/tests/test_task.py | 124 ++++++++++++++++++++++++++++++++++++-- 6 files changed, 135 insertions(+), 11 deletions(-) diff --git a/py-lib/Cargo.toml b/py-lib/Cargo.toml index 5fbb70264..6412105b6 100644 --- a/py-lib/Cargo.toml +++ b/py-lib/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["cdylib"] [dependencies] chrono.workspace = true -pyo3 = { verison = "0.20.0", features = ["anyhow"] } +pyo3 = { version = "0.20.0", features = ["anyhow"] } taskchampion = { path = "../taskchampion", version = "0.5.0" } anyhow = "*" diff --git a/py-lib/notes.md b/py-lib/notes.md index 3f4c9d851..d7972dc6a 100644 --- a/py-lib/notes.md +++ b/py-lib/notes.md @@ -2,4 +2,4 @@ - renamed the package to taskchampion, instead of py_lib, so the python imports work nicely - Implementing 2nd python class that is mutable and that has it's own mutable methods should be easier than trying to use single object that can have both states. -- Scratch that, sanely wrapping TaskMut with a lifetime param is not possible w/ pyo3, python just cannot handle lifetimes. So what to do with this? Should I go unsafe route or if there is a gimmick I could exploit, akin to C-lib? +- Scratch that, sanely wrapping TaskMut with a lifetime param is not possible w/ pyo3, python just cannot handle lifetimes diff --git a/py-lib/src/replica.rs b/py-lib/src/replica.rs index 10202347b..e34e67cda 100644 --- a/py-lib/src/replica.rs +++ b/py-lib/src/replica.rs @@ -3,7 +3,7 @@ use std::rc::Rc; use crate::status::Status; use crate::{DependencyMap, Task, WorkingSet}; -use pyo3::{exceptions::PyOSError, prelude::*}; +use pyo3::prelude::*; use taskchampion::storage::{InMemoryStorage, SqliteStorage}; use taskchampion::{Replica as TCReplica, Uuid}; diff --git a/py-lib/src/tag.rs b/py-lib/src/tag.rs index e4479957a..a564f95b8 100644 --- a/py-lib/src/tag.rs +++ b/py-lib/src/tag.rs @@ -1,13 +1,17 @@ use pyo3::prelude::*; use taskchampion::Tag as TCTag; -/// TODO: following the api there currently is no way to construct the task, not sure if this is +/// TODO: following the api there currently is no way to construct the task by hand, not sure if this is /// correct #[pyclass] pub struct Tag(pub(crate) TCTag); #[pymethods] impl Tag { + #[new] + pub fn new(tag: String) -> anyhow::Result { + Ok(Tag(tag.parse()?)) + } pub fn is_synthetic(&self) -> bool { self.0.is_synthetic() } diff --git a/py-lib/src/task.rs b/py-lib/src/task.rs index fcb654720..319a637e6 100644 --- a/py-lib/src/task.rs +++ b/py-lib/src/task.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; + use crate::{status::Status, Annotation, Tag}; use pyo3::prelude::*; -use taskchampion::{Task as TCTask, TaskMut as TCTaskMut}; +use taskchampion::Task as TCTask; // TODO: actually create a front-facing user class, instead of this data blob #[pyclass] pub struct Task(pub(crate) TCTask); @@ -24,8 +26,8 @@ impl Task { self.0.get_status().into() } - pub fn get_taskmap(&self) -> PyResult<()> { - unimplemented!() + pub fn get_taskmap(&self) -> HashMap { + self.0.get_taskmap().clone() } /// Get the entry timestamp for a task /// @@ -86,6 +88,8 @@ impl Task { /// /// Returns: /// bool: if the task has a given tag + // TODO: Not very user friendly; User has to construct a Tag object and then pass is into here. + // Should probably use a string pub fn has_tag(&self, tag: &Tag) -> bool { self.0.has_tag(&tag.0) } diff --git a/py-lib/tests/test_task.py b/py-lib/tests/test_task.py index 855d6610f..b70a4ba5d 100644 --- a/py-lib/tests/test_task.py +++ b/py-lib/tests/test_task.py @@ -1,10 +1,126 @@ -from taskchampion import Task, Replica +from taskchampion import Task, Replica, Status, Tag import pytest +from uuid import UUID @pytest.fixture def new_task(tmp_path): r = Replica(str(tmp_path), True) - r.get - return Task() - pass + task = r.new_task(Status.Pending, "Task 1") + return task + + +@pytest.fixture +def waiting_task(tmp_path): + r = Replica(str(tmp_path), True) + task = r.new_task(Status.Pending, "Task 1") + uuid = task.get_uuid() + r.update_task(uuid, "priority", "10") + # Fragile test, but I cannot mock Rust's Chrono, so this will do. + # This is the largest possible unix timestamp, so the tests should work + # until 2038 o7 + r.update_task(uuid, "wait", "2147483647") + r.update_task(uuid, "tag", "sample_tag") + # Need to refresh the tag, the one that's in memory is stale + task = r.get_task(uuid) + return task + + +@pytest.fixture +def started_task(tmp_path): + r = Replica(str(tmp_path), True) + task = r.new_task(Status.Pending, "Task 1") + uuid = task.get_uuid() + r.update_task(uuid, "start", "1147483647") + # Need to refresh the tag, the one that's in memory is stale + task = r.get_task(uuid) + return task + + +@pytest.fixture +def blocked_task(tmp_path): + r = Replica(str(tmp_path), True) + task = r.new_task(Status.Pending, "Task 1") + uuid = task.get_uuid() + r.update_task(uuid, "start", "1147483647") + # 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 + task = r.get_task(uuid) + return task + + +@pytest.fixture +def due_task(tmp_path): + r = Replica(str(tmp_path), True) + task = r.new_task(Status.Pending, "Task 1") + uuid = task.get_uuid() + r.update_task(uuid, "due", "1147483647") + # Need to refresh the tag, the one that's in memory is stale + task = r.get_task(uuid) + return task + + +def test_get_uuid(new_task: Task): + uuid = new_task.get_uuid() + assert uuid is not None + UUID(uuid) # This tests that the UUID is valid, it raises exception if not + + +@pytest.mark.skip("This could be a bug") +def test_get_status(new_task: Task): + status = new_task.get_status() + + assert status is Status.Pending + + +def test_get_taskmap(new_task: Task): + taskmap = new_task.get_taskmap() + + for key in taskmap.keys(): + assert key in ["modified", "description", "entry", "status"] + + +def test_get_priority(waiting_task: Task): + priority = waiting_task.get_priority() + assert priority == "10" + + +def test_get_wait(waiting_task: Task): + wait = waiting_task.get_wait() + assert wait == "2038-01-19T03:14:07+00:00" + + +def test_is_waiting(waiting_task: Task): + assert waiting_task.is_waiting() + + +def test_is_active(started_task: Task): + assert started_task.is_active() + + +@pytest.mark.skip() +def test_is_blocked(started_task: Task): + assert started_task.is_blocked() + + +@pytest.mark.skip() +def test_is_blocking(started_task: Task): + assert started_task.is_blocking() + + +@pytest.mark.skip("Enable this when able to add tags to the tasks") +def test_has_tag(waiting_task: Task): + assert waiting_task.has_tag(Tag("sample_tag")) + + +@pytest.mark.skip("Enable this when able to add tags to the tasks") +def test_get_tags(waiting_task: Task): + assert waiting_task.get_tags() + + +def test_get_modified(waiting_task: Task): + assert waiting_task.get_modified() is not None + + +def test_get_due(due_task: Task): + assert due_task.get_due() == "2006-05-13T01:27:27+00:00" From bf603fd1c905e85559d4d5027426cf409e64ce62 Mon Sep 17 00:00:00 2001 From: Illya Date: Mon, 13 May 2024 14:49:29 +0300 Subject: [PATCH 15/20] chore: replace notes.md with a permanent README.md --- py-lib/README.md | 10 ++++++++++ py-lib/notes.md | 5 ----- 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 py-lib/README.md delete mode 100644 py-lib/notes.md diff --git a/py-lib/README.md b/py-lib/README.md new file mode 100644 index 000000000..e0589e680 --- /dev/null +++ b/py-lib/README.md @@ -0,0 +1,10 @@ +# Python Taskchampion Bindings + +This submodule contains bindings to the Taskchampion + +# TODO + +- There is no good way to describe functions that accept interface (e.g. `Replica::new` accepts any of the storage implementations, but Python bindings lack such mechanisms), currently, `Replica::new` just constructs the SqliteStorage from the params passed into the constructor. +- Currently Task class is just a reflection of the rust's `Task` struct, but constructing the standalone `TaskMut` is impossible, as Pyo3 bindings do not allow lifetimes (python has no alternatives to them). Would be nice to expand the `Task` class to include the methods from `TaskMut` and convert into the mutable state and back when they are called. +- It is possible to convert `WorkingSet` into a python iterator (you can iterate over it via `for item in :` or `next()`), but that needs a way to store the current state. + diff --git a/py-lib/notes.md b/py-lib/notes.md deleted file mode 100644 index d7972dc6a..000000000 --- a/py-lib/notes.md +++ /dev/null @@ -1,5 +0,0 @@ -# Notes while developing the project - -- renamed the package to taskchampion, instead of py_lib, so the python imports work nicely -- Implementing 2nd python class that is mutable and that has it's own mutable methods should be easier than trying to use single object that can have both states. -- Scratch that, sanely wrapping TaskMut with a lifetime param is not possible w/ pyo3, python just cannot handle lifetimes From ffae5b319a7e38957c9d422c8506e5bd800a5e5e Mon Sep 17 00:00:00 2001 From: Illya Date: Mon, 13 May 2024 15:11:16 +0300 Subject: [PATCH 16/20] feat: test Tag class --- Cargo.lock | 20 ++++++++++---------- py-lib/Cargo.toml | 2 +- py-lib/src/lib.rs | 2 +- py-lib/tests/test_status.py | 0 py-lib/tests/test_tag.py | 23 ++++++++++++++++++++++- py-lib/tests/test_task.py | 2 ++ 6 files changed, 36 insertions(+), 13 deletions(-) delete mode 100644 py-lib/tests/test_status.py diff --git a/Cargo.lock b/Cargo.lock index 4ff9f39a9..1141e446c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1173,9 +1173,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.20.3" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" +checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" dependencies = [ "anyhow", "cfg-if", @@ -1192,9 +1192,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.20.3" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" +checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" dependencies = [ "once_cell", "target-lexicon", @@ -1202,9 +1202,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.20.3" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" +checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" dependencies = [ "libc", "pyo3-build-config", @@ -1212,9 +1212,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.20.3" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" +checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -1224,9 +1224,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.20.3" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" +checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" dependencies = [ "heck", "proc-macro2", diff --git a/py-lib/Cargo.toml b/py-lib/Cargo.toml index 6412105b6..d26c2f435 100644 --- a/py-lib/Cargo.toml +++ b/py-lib/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["cdylib"] [dependencies] chrono.workspace = true -pyo3 = { version = "0.20.0", features = ["anyhow"] } +pyo3 = { version = "0.21.2", features = ["anyhow"] } taskchampion = { path = "../taskchampion", version = "0.5.0" } anyhow = "*" diff --git a/py-lib/src/lib.rs b/py-lib/src/lib.rs index 9029841f0..f911c7c62 100644 --- a/py-lib/src/lib.rs +++ b/py-lib/src/lib.rs @@ -17,7 +17,7 @@ pub mod dependency_map; use dependency_map::*; #[pymodule] -fn taskchampion(_py: Python, m: &PyModule) -> PyResult<()> { +fn taskchampion(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/py-lib/tests/test_status.py b/py-lib/tests/test_status.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/py-lib/tests/test_tag.py b/py-lib/tests/test_tag.py index 6a2ad9303..e0ec831d2 100644 --- a/py-lib/tests/test_tag.py +++ b/py-lib/tests/test_tag.py @@ -1 +1,22 @@ -# TODO: test the darn thing +import pytest +from taskchampion import Tag + + +@pytest.fixture +def user_tag(): + return Tag("user_tag") + + +@pytest.fixture +def synthetic_tag(): + return Tag("UNBLOCKED") + + +def test_user_tag(user_tag: Tag): + assert user_tag.is_user() + assert not user_tag.is_synthetic() + + +def test_synthetic_tag(synthetic_tag: Tag): + assert synthetic_tag.is_synthetic() + assert not synthetic_tag.is_user() diff --git a/py-lib/tests/test_task.py b/py-lib/tests/test_task.py index b70a4ba5d..67424d089 100644 --- a/py-lib/tests/test_task.py +++ b/py-lib/tests/test_task.py @@ -70,6 +70,8 @@ def test_get_uuid(new_task: Task): def test_get_status(new_task: Task): status = new_task.get_status() + # for whatever reason these are not equivalent + # TODO: research if this is a bug assert status is Status.Pending From 133626b99af046954e9777edb07d084ea8977c71 Mon Sep 17 00:00:00 2001 From: Illya Date: Mon, 13 May 2024 15:32:22 +0300 Subject: [PATCH 17/20] chore: create poetry lock file --- py-lib/poetry.lock | 101 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 py-lib/poetry.lock diff --git a/py-lib/poetry.lock b/py-lib/poetry.lock new file mode 100644 index 000000000..9760b7eec --- /dev/null +++ b/py-lib/poetry.lock @@ -0,0 +1,101 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.2.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, + {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2.0" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.8" +content-hash = "e58753576a17a2417154ee53ae26069ca828eebcfbcb66e67c22b69d75a1abc1" From 5c89c4e10c0ecfaa5b24de3051bdbb04c72cb81b Mon Sep 17 00:00:00 2001 From: Illya Date: Mon, 13 May 2024 15:42:07 +0300 Subject: [PATCH 18/20] chore: update poetry dependencies --- py-lib/poetry.lock | 51 ++++++++++++++++++++++++++++++++++++++++++- py-lib/pyproject.toml | 5 ++++- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/py-lib/poetry.lock b/py-lib/poetry.lock index 9760b7eec..ed0db584e 100644 --- a/py-lib/poetry.lock +++ b/py-lib/poetry.lock @@ -36,6 +36,36 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "maturin" +version = "1.5.1" +description = "Build and publish crates with pyo3, rust-cpython and cffi bindings as well as rust binaries as python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "maturin-1.5.1-py3-none-linux_armv6l.whl", hash = "sha256:589e9b7024007e130b136ba6f1c2c8393a87e42cf968d12852913ab1e3c69ed3"}, + {file = "maturin-1.5.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a1abda07093b3c8ef897626166c02ed64e3e446c48460b28efb51833abf89cbb"}, + {file = "maturin-1.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:48a1fbbdc2514525f27d6d339ab97b098ede28759f8593d110c89cc07bbe40ed"}, + {file = "maturin-1.5.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:96d96b1fa3a165db9ca539f764d31da8ebc92e31ca3a1dd6ccd50008d222bd96"}, + {file = "maturin-1.5.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:786bf36a98c4e27cbebb1dc8e432c1bcbbb59e7a9719592cbb89e46a0ccd5bcc"}, + {file = "maturin-1.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:d821b37da759884ad09cfee4cd9deac10f4132744cc66e4d9190a1972233bc83"}, + {file = "maturin-1.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:62133bf690555bbc8cc6b1c18a0c57b0ab2b4d68d3fcd320eb16df941563fe06"}, + {file = "maturin-1.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:6bff165252b1fcc887679ddf7b71b5cc024327ba96ea893133be38c0ed38f163"}, + {file = "maturin-1.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2c42a95466ffc3de0a3940cd20c57cf0c44fe5ea679375d73422afbb00236c64"}, + {file = "maturin-1.5.1-py3-none-win32.whl", hash = "sha256:d09538b4aa0da4b59fd47cb429003b45bfd5d801714adf1db2511bf8bdea532f"}, + {file = "maturin-1.5.1-py3-none-win_amd64.whl", hash = "sha256:a3db9054222ac79275e082b21cfd234b8e036714a4ff227a0a28f6a3ffa3744d"}, + {file = "maturin-1.5.1-py3-none-win_arm64.whl", hash = "sha256:acf528e51413f6ae489473d64116d8c83f140386349004949d29137c16a82193"}, + {file = "maturin-1.5.1.tar.gz", hash = "sha256:3dd834ece80edb866af18cbd4635e0ecac40139c726428d5f1849ae154b26dca"}, +] + +[package.dependencies] +patchelf = {version = "*", optional = true, markers = "extra == \"patchelf\""} +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} + +[package.extras] +patchelf = ["patchelf"] +zig = ["ziglang (>=0.10.0,<0.11.0)"] + [[package]] name = "packaging" version = "24.0" @@ -47,6 +77,25 @@ files = [ {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] +[[package]] +name = "patchelf" +version = "0.17.2.1" +description = "A small utility to modify the dynamic linker and RPATH of ELF executables." +optional = false +python-versions = "*" +files = [ + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:fc329da0e8f628bd836dfb8eaf523547e342351fa8f739bf2b3fe4a6db5a297c"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:ccb266a94edf016efe80151172c26cff8c2ec120a57a1665d257b0442784195d"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:f47b5bdd6885cfb20abdd14c707d26eb6f499a7f52e911865548d4aa43385502"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.musllinux_1_1_s390x.whl", hash = "sha256:a9e6ebb0874a11f7ed56d2380bfaa95f00612b23b15f896583da30c2059fcfa8"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_i686.manylinux1_i686.musllinux_1_1_i686.whl", hash = "sha256:3c8d58f0e4c1929b1c7c45ba8da5a84a8f1aa6a82a46e1cfb2e44a4d40f350e5"}, + {file = "patchelf-0.17.2.1-py2.py3-none-manylinux_2_5_x86_64.manylinux1_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:d1a9bc0d4fd80c038523ebdc451a1cce75237cfcc52dbd1aca224578001d5927"}, + {file = "patchelf-0.17.2.1.tar.gz", hash = "sha256:a6eb0dd452ce4127d0d5e1eb26515e39186fa609364274bc1b0b77539cfa7031"}, +] + +[package.extras] +test = ["importlib-metadata", "pytest"] + [[package]] name = "pluggy" version = "1.5.0" @@ -98,4 +147,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "e58753576a17a2417154ee53ae26069ca828eebcfbcb66e67c22b69d75a1abc1" +content-hash = "c9d9ab0419e12f54af2811be597ee037faed3da6dcdc3817ab03c354ddbbf6ea" diff --git a/py-lib/pyproject.toml b/py-lib/pyproject.toml index 4149728de..0791a07fd 100644 --- a/py-lib/pyproject.toml +++ b/py-lib/pyproject.toml @@ -23,4 +23,7 @@ description = "" [tool.poetry.dependencies] python = ">=3.8" -pytest = "*" +maturin = {extras = ["patchelf"], version = "^1.5.1"} + +[tool.poetry.group.test.dependencies] +pytest = "^8.2.0" From 45bc276272c3541feacc3f7349d281d97acd2c48 Mon Sep 17 00:00:00 2001 From: Illya Date: Thu, 23 May 2024 17:20:08 +0300 Subject: [PATCH 19/20] feat: add python stubs --- py-lib/mypy.ini | 8 +++ py-lib/poetry.lock | 77 ++++++++++++++++++++++-- py-lib/pyproject.toml | 1 + py-lib/taskchampion.pyi | 113 +++++++++++++++++++++++++++++++++++ py-lib/tests/test_replica.py | 9 +-- 5 files changed, 200 insertions(+), 8 deletions(-) create mode 100644 py-lib/mypy.ini create mode 100644 py-lib/taskchampion.pyi diff --git a/py-lib/mypy.ini b/py-lib/mypy.ini new file mode 100644 index 000000000..1d270f371 --- /dev/null +++ b/py-lib/mypy.ini @@ -0,0 +1,8 @@ +[mypy] +warn_return_any = True +warn_unused_configs = True +no_implicit_optional = True +check_untyped_defs = True +warn_unused_ignores = True +show_error_codes = True +disable_error_code = assignment diff --git a/py-lib/poetry.lock b/py-lib/poetry.lock index ed0db584e..e27a25df2 100644 --- a/py-lib/poetry.lock +++ b/py-lib/poetry.lock @@ -66,6 +66,64 @@ tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} patchelf = ["patchelf"] zig = ["ziglang (>=0.10.0,<0.11.0)"] +[[package]] +name = "mypy" +version = "1.10.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2"}, + {file = "mypy-1.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99"}, + {file = "mypy-1.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2"}, + {file = "mypy-1.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9"}, + {file = "mypy-1.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"}, + {file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"}, + {file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"}, + {file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"}, + {file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"}, + {file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"}, + {file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"}, + {file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"}, + {file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0"}, + {file = "mypy-1.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727"}, + {file = "mypy-1.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4"}, + {file = "mypy-1.10.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061"}, + {file = "mypy-1.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976"}, + {file = "mypy-1.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec"}, + {file = "mypy-1.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821"}, + {file = "mypy-1.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746"}, + {file = "mypy-1.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a"}, + {file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"}, + {file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "packaging" version = "24.0" @@ -113,13 +171,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pytest" -version = "8.2.0" +version = "8.2.1" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, - {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, + {file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"}, + {file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"}, ] [package.dependencies] @@ -144,7 +202,18 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + [metadata] lock-version = "2.0" python-versions = ">=3.8" -content-hash = "c9d9ab0419e12f54af2811be597ee037faed3da6dcdc3817ab03c354ddbbf6ea" +content-hash = "efe06dc2a8ee89e602f4a79386f48534d2c6f624288360a7e8dd558ff296f6f2" diff --git a/py-lib/pyproject.toml b/py-lib/pyproject.toml index 0791a07fd..a9cdf4577 100644 --- a/py-lib/pyproject.toml +++ b/py-lib/pyproject.toml @@ -27,3 +27,4 @@ maturin = {extras = ["patchelf"], version = "^1.5.1"} [tool.poetry.group.test.dependencies] pytest = "^8.2.0" +mypy = "^1.10.0" diff --git a/py-lib/taskchampion.pyi b/py-lib/taskchampion.pyi new file mode 100644 index 000000000..0b88710f7 --- /dev/null +++ b/py-lib/taskchampion.pyi @@ -0,0 +1,113 @@ +from typing import Optional +from enum import Enum + + +class Replica: + def __init__(self, path: str, create_if_missing: bool): ... + + def new_task(self, status: "Status", description: str) -> "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"]: ... + + def import_task_with_uuid(self, uuid: str) -> "Task": ... + + def sync(self): ... + + 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: ... + + +class Status(Enum): + Pending = 1 + Completed = 2 + Deleted = 3 + Recurring = 4 + Unknown = 5 + + +class Task: + def get_uuid(self) -> str: ... + + def get_status(self) -> "Status": ... + + def get_taskmap(self) -> dict[str, str]: ... + + def get_entry(self) -> Optional[str]: ... + + def get_priority(self) -> str: ... + + def get_wait(self) -> Optional[str]: ... + + def is_waiting(self) -> bool: ... + + def is_active(self) -> bool: ... + + def is_blocked(self) -> bool: ... + + def is_blocking(self) -> bool: ... + + def has_tag(self, tag: "Tag") -> bool: ... + + def get_tags(self) -> list["Tag"]: ... + + def get_annotations(self) -> list["Annotation"]: ... + + def get_uda(self, namespace: str, key: str) -> Optional[str]: ... + + def get_udas(self) -> list[tuple[tuple[str, str], str]]: ... + + def get_modified(self) -> Optional[str]: ... + + def get_due(self) -> Optional[str]: ... + + def get_dependencies(self) -> list[str]: ... + + def get_value(self, property: str) -> Optional[str]: ... + + +class WorkingSet: + def __len__(self) -> int: ... + + def largest_index(self) -> int: ... + + def is_empty(self) -> bool: ... + + def by_index(self, index: int) -> Optional[str]: ... + + def by_uuid(self, uuid: str) -> Optional[int]: ... + + +class Annotation: + entry: str + description: str + + def __init__(self) -> None: ... + + +class DependencyMap: + def dependencies(self, dep_of: str) -> list[str]: ... + + def dependents(self, dep_on: str) -> list[str]: ... + + +class Tag: + def __init__(self, tag: str): ... + def is_synthetic(self) -> bool: ... + def is_user(self) -> bool: ... diff --git a/py-lib/tests/test_replica.py b/py-lib/tests/test_replica.py index 5e5cd4c56..d98ed1c41 100644 --- a/py-lib/tests/test_replica.py +++ b/py-lib/tests/test_replica.py @@ -1,4 +1,4 @@ -from taskchampion import Replica, Status, WorkingSet +from taskchampion import Replica, Status import pytest from pathlib import Path @@ -64,7 +64,7 @@ def test_all_tasks(empty_replica: Replica): def test_working_set(replica_with_tasks: Replica): - ws: WorkingSet = replica_with_tasks.working_set() + ws = replica_with_tasks.working_set() assert ws is not None @@ -80,11 +80,13 @@ def test_get_task(replica_with_tasks: Replica): assert task is not None +@pytest.mark.skip() def test_rebuild_working_set(replica_with_tasks: Replica): # TODO actually test this replica_with_tasks.rebuild_working_set(False) +@pytest.mark.skip() def test_add_undo_point(replica_with_tasks: Replica): replica_with_tasks.add_undo_point(False) @@ -117,7 +119,6 @@ def test_dependency_map(replica_with_tasks: Replica): def test_update_task(replica_with_tasks: Replica): task_uuid = replica_with_tasks.all_task_uuids()[0] - res = replica_with_tasks.update_task( - task_uuid, "description", "This text here") + res = replica_with_tasks.update_task(task_uuid, "description", "This text here") assert res["description"] == "This text here" assert res["status"] == "pending" From 599a1aa30b41061457f54d59e3b4a545e9c64177 Mon Sep 17 00:00:00 2001 From: Illya Laifu Date: Thu, 15 Aug 2024 15:43:18 +0300 Subject: [PATCH 20/20] feat:sync with the upstream --- Cargo.lock | 96 ++++++++++++++++++++++++++++++----------------- Cargo.toml | 1 + py-lib/Cargo.toml | 4 +- 3 files changed, 65 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e46084f0c..6bc448d3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -618,6 +618,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -754,6 +760,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + [[package]] name = "ipnet" version = "2.9.0" @@ -1018,16 +1030,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] -name = "powerfmt" -version = "0.2.0" +name = "portable-atomic" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" [[package]] -name = "portable-atomic" -version = "1.6.0" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" @@ -1076,16 +1088,16 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.21.2" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8" +checksum = "831e8e819a138c36e212f3af3fd9eeffed6bf1510a805af35b0edee5ffa59433" dependencies = [ "anyhow", "cfg-if", "indoc", "libc", "memoffset", - "parking_lot", + "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", @@ -1095,9 +1107,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.21.2" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50" +checksum = "1e8730e591b14492a8945cdff32f089250b05f5accecf74aeddf9e8272ce1fa8" dependencies = [ "once_cell", "target-lexicon", @@ -1105,9 +1117,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.21.2" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403" +checksum = "5e97e919d2df92eb88ca80a037969f44e5e70356559654962cbb3316d00300c6" dependencies = [ "libc", "pyo3-build-config", @@ -1115,27 +1127,27 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.21.2" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c" +checksum = "eb57983022ad41f9e683a599f2fd13c3664d7063a3ac5714cae4b7bee7d3f206" dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.18", + "syn 2.0.66", ] [[package]] name = "pyo3-macros-backend" -version = "0.21.2" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c" +checksum = "ec480c0c51ddec81019531705acac51bcdbeae563557c982aa8263bb96880372" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.18", + "syn 2.0.66", ] [[package]] @@ -1606,7 +1618,7 @@ version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ - "heck", + "heck 0.4.1", "proc-macro2", "quote", "rustversion", @@ -1641,11 +1653,38 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "target-lexicon" -version = "0.12.14" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "taskchampion" @@ -1674,17 +1713,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "taskchampion-lib" -version = "0.5.0" -dependencies = [ - "anyhow", - "ffizz-header", - "libc", - "pretty_assertions", - "taskchampion", -] - [[package]] name = "taskchampion_python" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index bf658d16b..a621ecfd0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "taskchampion", "xtask", + "py-lib" ] resolver = "2" diff --git a/py-lib/Cargo.toml b/py-lib/Cargo.toml index d26c2f435..1f9c61de3 100644 --- a/py-lib/Cargo.toml +++ b/py-lib/Cargo.toml @@ -12,7 +12,7 @@ crate-type = ["cdylib"] [dependencies] chrono.workspace = true -pyo3 = { version = "0.21.2", features = ["anyhow"] } +pyo3 = { version = "0.22.0", features = ["anyhow"] } -taskchampion = { path = "../taskchampion", version = "0.5.0" } +taskchampion = { path = "../taskchampion", version = "0.7.0" } anyhow = "*"