From e37435b485ee898a526401753672bedab4a27f54 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 5 Aug 2024 16:44:51 -0700 Subject: [PATCH 1/8] Rename "Untitled Checks Framework" in Python docs to "Open Checks Framework" Signed-off-by: Scott Wilson --- bindings/python/docs/source/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/python/docs/source/conf.py b/bindings/python/docs/source/conf.py index f02d37b..1f09e8b 100644 --- a/bindings/python/docs/source/conf.py +++ b/bindings/python/docs/source/conf.py @@ -17,8 +17,8 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = "Untitled Checks Framework" -copyright = "2022, Scott Wilson" +project = "Open Checks Framework" +copyright = "2024, Scott Wilson" author = "Scott Wilson" release = "0.1.0" From 0ce1e1a93a4299cd86b82e1c25f5d006ed879ae8 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 5 Aug 2024 20:05:00 -0700 Subject: [PATCH 2/8] Improve Rust documentation Signed-off-by: Scott Wilson --- .github/workflows/test_suite_rust.yml | 2 + Cargo.toml | 7 +- src/check.rs | 312 +++++++++++++++++++++++++- src/error.rs | 2 + src/lib.rs | 8 + src/runner.rs | 289 ++++++++++++++++++++++++ 6 files changed, 618 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_suite_rust.yml b/.github/workflows/test_suite_rust.yml index 568a6a1..3f77a43 100644 --- a/.github/workflows/test_suite_rust.yml +++ b/.github/workflows/test_suite_rust.yml @@ -52,6 +52,8 @@ jobs: if: ${{ matrix.os == 'ubuntu-latest' }} - name: Generate coverage report from Rust tests run: cargo llvm-cov --all-features --lcov --output-path lcov.info + - name: Run doc tests + run: cargo test --doc - uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0 with: files: lcov.info diff --git a/Cargo.toml b/Cargo.toml index 5636458..f9c2ce2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,12 @@ async-trait = "0.1.81" bitflags = "2.6.0" [dev-dependencies] -tokio = { version = "1.38.1", features = ["time", "rt", "macros"] } +tokio = { version = "1.38.1", features = [ + "time", + "rt", + "rt-multi-thread", + "macros", +] } [features] arbitrary = ["dep:arbitrary", "bitflags/arbitrary"] diff --git a/src/check.rs b/src/check.rs index a4e7893..a7ed8f0 100644 --- a/src/check.rs +++ b/src/check.rs @@ -19,7 +19,20 @@ bitflags::bitflags! { } } -/// The base check. +/// The check metadata. +/// +/// This stores the information about the check that is either useful for humans +/// (the [title](CheckMetadata::title) and +/// [description](CheckMetadata::description)) or useful for systems that uses +/// the check ([hint](CheckMetadata::hint)). For example, a user interface could +/// use the title and description to render information for an artist to inform +/// them about what the check will validate and how it will fix issues (if +/// supported). The hint then could be used to render other useful information +/// such as whether the check supports automatic fixes in general, whether it +/// could be overridden by a supervisor, etc. +/// +/// This is one of the two traits (this and the [Check] or [AsyncCheck]) that +/// must be implemented for the check system to run. pub trait CheckMetadata { /// The human readable title for the check. /// @@ -40,6 +53,146 @@ pub trait CheckMetadata { } /// The check trait. +/// +/// This is responsible for validating the input data and returning a result +/// such as pass or fail. It can also provide extra data such as what caused the +/// status (for example, the scene nodes that are named incorrectly). +/// +/// If the check supports it, then the data being validated can be automatically +/// fixed. +/// +/// # Examples +/// +/// ## Simple Check +/// +/// ```rust +/// # use openchecks::{CheckResult, Item, Check, CheckMetadata, Status, run}; +/// # +/// # #[derive(Debug, PartialEq, PartialOrd)] +/// # struct IntItem(i32); +/// # +/// # impl std::fmt::Display for IntItem { +/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// # self.0.fmt(f) +/// # } +/// # } +/// # +/// # impl Item for IntItem { +/// # type Value = i32; +/// # +/// # fn value(&self) -> Self::Value { +/// # self.0 +/// # } +/// # } +/// # +/// struct IsEqualCheck(i32); +/// +/// impl CheckMetadata for IsEqualCheck { +/// fn title(&self) -> std::borrow::Cow { +/// "Is Equal Check".into() +/// } +/// +/// fn description(&self) -> std::borrow::Cow { +/// "Check if the number is equal.".into() +/// } +/// } +/// +/// impl Check for IsEqualCheck { +/// type Item = IntItem; +/// +/// type Items = Vec; +/// +/// fn check(&self) -> CheckResult { +/// if self.0 % 2 == 0 { +/// CheckResult::new_passed("The number is even.", None, false, false) +/// } else { +/// CheckResult::new_failed("The number is not even.", None, false, false) +/// } +/// } +/// } +/// +/// impl IsEqualCheck { +/// pub fn new(value: i32) -> Self{ +/// Self(value) +/// } +/// } +/// +/// let check = IsEqualCheck::new(2); +/// let result = run(&check); +/// +/// assert_eq!(*result.status(), Status::Passed); +/// ``` +/// +/// ## Check with Automatic Fix +/// +/// ```rust +/// # use openchecks::{CheckResult, Item, Check, CheckMetadata, Status, Error, auto_fix, run}; +/// # +/// # #[derive(Debug, PartialEq, PartialOrd)] +/// # struct IntItem(i32); +/// # +/// # impl std::fmt::Display for IntItem { +/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// # self.0.fmt(f) +/// # } +/// # } +/// # +/// # impl Item for IntItem { +/// # type Value = i32; +/// # +/// # fn value(&self) -> Self::Value { +/// # self.0 +/// # } +/// # } +/// # +/// struct IsZeroCheck(i32); +/// +/// impl CheckMetadata for IsZeroCheck { +/// fn title(&self) -> std::borrow::Cow { +/// "Is Zero Check".into() +/// } +/// +/// fn description(&self) -> std::borrow::Cow { +/// "Check if the number is zero.".into() +/// } +/// } +/// +/// impl Check for IsZeroCheck { +/// type Item = IntItem; +/// +/// type Items = Vec; +/// +/// fn check(&self) -> CheckResult { +/// if self.0 == 0 { +/// CheckResult::new_passed("Good", None, true, false) +/// } else { +/// CheckResult::new_failed("Bad", None, true, false) +/// } +/// } +/// +/// fn auto_fix(&mut self) -> Result<(), Error> { +/// self.0 = 0; +/// Ok(()) +/// } +/// } +/// +/// impl IsZeroCheck { +/// pub fn new(value: i32) -> Self{ +/// Self(value) +/// } +/// } +/// +/// let mut check = IsZeroCheck::new(1); +/// let result = run(&check); +/// +/// assert_eq!(*result.status(), Status::Failed); +/// +/// if result.can_fix() { +/// let result = auto_fix(&mut check); +/// assert_eq!(*result.status(), Status::Passed); +/// } +/// +/// ``` pub trait Check: CheckMetadata { /// The item type is a wrapper around the object(s) that caused the result. type Item: crate::Item; @@ -53,12 +206,164 @@ pub trait Check: CheckMetadata { /// Automatically fix the issue detected by the [check](crate::Check::check) /// method. + /// + /// # Errors + /// + /// Will return an error if the `auto_fix` is not implemented, or an error + /// happened in the `auto_fix`, such as a filesystem error. fn auto_fix(&mut self) -> Result<(), crate::Error> { Err(crate::Error::new("Auto fix is not implemented.")) } } /// The check trait, but supporting async. +/// +/// This is responsible for validating the input data and returning a result +/// such as pass or fail. It can also provide extra data such as what caused the +/// status (for example, the scene nodes that are named incorrectly). +/// +/// If the check supports it, then the data being validated can be automatically +/// fixed. +/// +/// # Examples +/// +/// ## Simple Check +/// +/// ```rust +/// # use openchecks::{CheckResult, Item, AsyncCheck, CheckMetadata, Status, async_run}; +/// # +/// # #[derive(Debug, PartialEq, PartialOrd)] +/// # struct IntItem(i32); +/// # +/// # impl std::fmt::Display for IntItem { +/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// # self.0.fmt(f) +/// # } +/// # } +/// # +/// # impl Item for IntItem { +/// # type Value = i32; +/// # +/// # fn value(&self) -> Self::Value { +/// # self.0 +/// # } +/// # } +/// # +/// struct IsEqualCheck(i32); +/// +/// impl CheckMetadata for IsEqualCheck { +/// fn title(&self) -> std::borrow::Cow { +/// "Is Equal Check".into() +/// } +/// +/// fn description(&self) -> std::borrow::Cow { +/// "Check if the number is equal.".into() +/// } +/// } +/// +/// #[async_trait::async_trait] +/// impl AsyncCheck for IsEqualCheck { +/// type Item = IntItem; +/// +/// type Items = Vec; +/// +/// async fn async_check(&self) -> CheckResult { +/// if self.0 % 2 == 0 { +/// CheckResult::new_passed("The number is even.", None, false, false) +/// } else { +/// CheckResult::new_failed("The number is not even.", None, false, false) +/// } +/// } +/// } +/// +/// impl IsEqualCheck { +/// pub fn new(value: i32) -> Self{ +/// Self(value) +/// } +/// } +/// +/// # #[tokio::main] +/// # async fn main() { +/// let check = IsEqualCheck::new(2); +/// let result = async_run(&check).await; +/// +/// assert_eq!(*result.status(), Status::Passed); +/// # } +/// ``` +/// +/// ## Check with Automatic Fix +/// +/// ```rust +/// # use openchecks::{CheckResult, Item, AsyncCheck, CheckMetadata, Status, Error, async_auto_fix, async_run}; +/// # +/// # #[derive(Debug, PartialEq, PartialOrd)] +/// # struct IntItem(i32); +/// # +/// # impl std::fmt::Display for IntItem { +/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// # self.0.fmt(f) +/// # } +/// # } +/// # +/// # impl Item for IntItem { +/// # type Value = i32; +/// # +/// # fn value(&self) -> Self::Value { +/// # self.0 +/// # } +/// # } +/// # +/// struct IsZeroCheck(i32); +/// +/// impl CheckMetadata for IsZeroCheck { +/// fn title(&self) -> std::borrow::Cow { +/// "Is Zero Check".into() +/// } +/// +/// fn description(&self) -> std::borrow::Cow { +/// "Check if the number is zero.".into() +/// } +/// } +/// +/// #[async_trait::async_trait] +/// impl AsyncCheck for IsZeroCheck { +/// type Item = IntItem; +/// +/// type Items = Vec; +/// +/// async fn async_check(&self) -> CheckResult { +/// if self.0 == 0 { +/// CheckResult::new_passed("Good", None, true, false) +/// } else { +/// CheckResult::new_failed("Bad", None, true, false) +/// } +/// } +/// +/// async fn async_auto_fix(&mut self) -> Result<(), Error> { +/// self.0 = 0; +/// Ok(()) +/// } +/// } +/// +/// impl IsZeroCheck { +/// pub fn new(value: i32) -> Self{ +/// Self(value) +/// } +/// } +/// +/// # #[tokio::main] +/// # async fn main() { +/// let mut check = IsZeroCheck::new(1); +/// let result = async_run(&check).await; +/// +/// assert_eq!(*result.status(), Status::Failed); +/// +/// if result.can_fix() { +/// let result = async_auto_fix(&mut check).await; +/// assert_eq!(*result.status(), Status::Passed); +/// } +/// # } +/// ``` #[async_trait::async_trait] pub trait AsyncCheck: CheckMetadata { /// The item type is a wrapper around the object(s) that caused the result. @@ -73,6 +378,11 @@ pub trait AsyncCheck: CheckMetadata { /// Automatically fix the issue detected by the /// [check](crate::AsyncCheck::async_check) method. + /// + /// # Errors + /// + /// Will return an error if the `auto_fix` is not implemented, or an error + /// happened in the `auto_fix`, such as a filesystem error. async fn async_auto_fix(&mut self) -> Result<(), crate::Error> { Err(crate::Error::new("Auto fix is not implemented.")) } diff --git a/src/error.rs b/src/error.rs index c42dbc3..f0f9f72 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,3 +1,4 @@ +/// A simple error type. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Error { @@ -13,6 +14,7 @@ impl std::fmt::Display for Error { impl std::error::Error for Error {} impl Error { + /// Create a new error. pub fn new(message: &str) -> Self { Self { message: message.to_string(), diff --git a/src/lib.rs b/src/lib.rs index 7ca6c7a..9c0337a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,13 @@ #![forbid(unsafe_code)] #![doc = include_str!("../README.md")] +#![deny(clippy::empty_docs)] +#![deny(clippy::empty_line_after_doc_comments)] +#![deny(clippy::missing_errors_doc)] +#![deny(clippy::missing_panics_doc)] +#![deny(clippy::missing_safety_doc)] +#![deny(clippy::undocumented_unsafe_blocks)] +#![deny(clippy::unnecessary_safety_doc)] +#![deny(missing_docs)] mod check; mod error; diff --git a/src/runner.rs b/src/runner.rs index a7aad6d..ca1d256 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -1,4 +1,78 @@ /// Run a check. +/// +/// Running a check should never fail, but instead return a failure check +/// result. The run function might return a +/// [Status::SystemError](crate::Status::SystemError) if the system runs into an +/// error that must be resolved by the team supporting and implementing the +/// checks. +/// +/// However, if there is a panic, then this will not capture the panic, and +/// simply let the panic bubble up the stack since it assumes that the +/// environment is now in a bad and unrecoverable state. +/// +/// # Examples +/// +/// ```rust +/// # use openchecks::{CheckResult, Item, AsyncCheck, CheckMetadata, Status, async_run}; +/// # +/// # #[derive(Debug, PartialEq, PartialOrd)] +/// # struct IntItem(i32); +/// # +/// # impl std::fmt::Display for IntItem { +/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// # self.0.fmt(f) +/// # } +/// # } +/// # +/// # impl Item for IntItem { +/// # type Value = i32; +/// # +/// # fn value(&self) -> Self::Value { +/// # self.0 +/// # } +/// # } +/// # +/// # struct IsEqualCheck(i32); +/// # +/// # impl CheckMetadata for IsEqualCheck { +/// # fn title(&self) -> std::borrow::Cow { +/// # todo!() +/// # } +/// # +/// # fn description(&self) -> std::borrow::Cow { +/// # todo!() +/// # } +/// # } +/// # +/// # #[async_trait::async_trait] +/// # impl AsyncCheck for IsEqualCheck { +/// # type Item = IntItem; +/// # +/// # type Items = Vec; +/// # +/// # async fn async_check(&self) -> CheckResult { +/// # if self.0 % 2 == 0 { +/// # CheckResult::new_passed("Good", None, false, false) +/// # } else { +/// # CheckResult::new_failed("Bad", None, false, false) +/// # } +/// # } +/// # } +/// # +/// # impl IsEqualCheck { +/// # pub fn new(value: i32) -> Self{ +/// # Self(value) +/// # } +/// # } +/// # +/// # #[tokio::main] +/// # async fn main() { +/// let check = IsEqualCheck::new(2); +/// let result = async_run(&check).await; +/// +/// assert_eq!(*result.status(), Status::Passed); +/// # } +/// ``` pub async fn async_run>( check: &impl crate::AsyncCheck, ) -> crate::CheckResult { @@ -25,6 +99,80 @@ pub async fn async_run) -> std::fmt::Result { +/// # self.0.fmt(f) +/// # } +/// # } +/// # +/// # impl Item for IntItem { +/// # type Value = i32; +/// # +/// # fn value(&self) -> Self::Value { +/// # self.0 +/// # } +/// # } +/// # +/// # struct IsZeroCheck(i32); +/// # +/// # impl CheckMetadata for IsZeroCheck { +/// # fn title(&self) -> std::borrow::Cow { +/// # todo!() +/// # } +/// # +/// # fn description(&self) -> std::borrow::Cow { +/// # todo!() +/// # } +/// # } +/// # +/// # #[async_trait::async_trait] +/// # impl AsyncCheck for IsZeroCheck { +/// # type Item = IntItem; +/// # +/// # type Items = Vec; +/// # +/// # async fn async_check(&self) -> CheckResult { +/// # if self.0 == 0 { +/// # CheckResult::new_passed("Good", None, true, false) +/// # } else { +/// # CheckResult::new_failed("Bad", None, true, false) +/// # } +/// # } +/// # +/// # async fn async_auto_fix(&mut self) -> Result<(), Error> { +/// # self.0 = 0; +/// # Ok(()) +/// # } +/// # } +/// # +/// # impl IsZeroCheck { +/// # pub fn new(value: i32) -> Self{ +/// # Self(value) +/// # } +/// # } +/// # +/// # #[tokio::main] +/// # async fn main() { +/// let mut check = IsZeroCheck::new(1); +/// let result = async_run(&check).await; +/// +/// assert_eq!(*result.status(), Status::Failed); +/// +/// if result.can_fix() { +/// let result = async_auto_fix(&mut check).await; +/// assert_eq!(*result.status(), Status::Passed); +/// } +/// # } +/// ``` pub async fn async_auto_fix>( check: &mut (impl crate::AsyncCheck + Send), ) -> crate::CheckResult { @@ -66,6 +214,76 @@ pub async fn async_auto_fix) -> std::fmt::Result { +/// # self.0.fmt(f) +/// # } +/// # } +/// # +/// # impl Item for IntItem { +/// # type Value = i32; +/// # +/// # fn value(&self) -> Self::Value { +/// # self.0 +/// # } +/// # } +/// # +/// # struct IsEqualCheck(i32); +/// # +/// # impl CheckMetadata for IsEqualCheck { +/// # fn title(&self) -> std::borrow::Cow { +/// # todo!() +/// # } +/// # +/// # fn description(&self) -> std::borrow::Cow { +/// # todo!() +/// # } +/// # } +/// # +/// # impl Check for IsEqualCheck { +/// # type Item = IntItem; +/// # +/// # type Items = Vec; +/// # +/// # fn check(&self) -> CheckResult { +/// # if self.0 % 2 == 0 { +/// # CheckResult::new_passed("Good", None, false, false) +/// # } else { +/// # CheckResult::new_failed("Bad", None, false, false) +/// # } +/// # } +/// # } +/// # +/// # impl IsEqualCheck { +/// # pub fn new(value: i32) -> Self{ +/// # Self(value) +/// # } +/// # } +/// # +/// let check = IsEqualCheck::new(2); +/// let result = run(&check); +/// +/// assert_eq!(*result.status(), Status::Passed); +/// ``` pub fn run>( check: &impl crate::Check, ) -> crate::CheckResult { @@ -92,6 +310,77 @@ pub fn run>( /// not have the [CheckHint::AUTO_FIX](crate::CheckHint::AUTO_FIX) flag set, or /// an auto-fix returned an error. In the case of the latter, it will include /// the error with the check result. +/// +/// # Examples +/// +/// ```rust +/// # use openchecks::{CheckResult, Item, Check, CheckMetadata, Status, Error, auto_fix, run}; +/// # +/// # #[derive(Debug, PartialEq, PartialOrd)] +/// # struct IntItem(i32); +/// # +/// # impl std::fmt::Display for IntItem { +/// # fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { +/// # self.0.fmt(f) +/// # } +/// # } +/// # +/// # impl Item for IntItem { +/// # type Value = i32; +/// # +/// # fn value(&self) -> Self::Value { +/// # self.0 +/// # } +/// # } +/// # +/// # struct IsZeroCheck(i32); +/// # +/// # impl CheckMetadata for IsZeroCheck { +/// # fn title(&self) -> std::borrow::Cow { +/// # todo!() +/// # } +/// # +/// # fn description(&self) -> std::borrow::Cow { +/// # todo!() +/// # } +/// # } +/// # +/// # impl Check for IsZeroCheck { +/// # type Item = IntItem; +/// # +/// # type Items = Vec; +/// # +/// # fn check(&self) -> CheckResult { +/// # if self.0 == 0 { +/// # CheckResult::new_passed("Good", None, true, false) +/// # } else { +/// # CheckResult::new_failed("Bad", None, true, false) +/// # } +/// # } +/// # +/// # fn auto_fix(&mut self) -> Result<(), Error> { +/// # self.0 = 0; +/// # Ok(()) +/// # } +/// # } +/// # +/// # impl IsZeroCheck { +/// # pub fn new(value: i32) -> Self{ +/// # Self(value) +/// # } +/// # } +/// # +/// let mut check = IsZeroCheck::new(1); +/// let result = run(&check); +/// +/// assert_eq!(*result.status(), Status::Failed); +/// +/// if result.can_fix() { +/// let result = auto_fix(&mut check); +/// assert_eq!(*result.status(), Status::Passed); +/// } +/// +/// ``` pub fn auto_fix>( check: &mut impl crate::Check, ) -> crate::CheckResult { From 045be3ff6afb70d4e249eabd19a6febb14945ef8 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 5 Aug 2024 22:04:19 -0700 Subject: [PATCH 3/8] Improve Python docs Signed-off-by: Scott Wilson --- .github/workflows/test_suite_python.yml | 9 +- bindings/python/src/check.rs | 195 +++++++++++++++++++++++- bindings/python/src/item.rs | 56 +++++++ bindings/python/src/runner.rs | 178 +++++++++++++++++++++ 4 files changed, 432 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_suite_python.yml b/.github/workflows/test_suite_python.yml index 9129faa..6994f38 100644 --- a/.github/workflows/test_suite_python.yml +++ b/.github/workflows/test_suite_python.yml @@ -74,7 +74,7 @@ jobs: python-version: ${{matrix.python-version}} cache: 'pip' - name: Install dependencies - run: python -m pip install .[test] + run: python -m pip install .[test,docs] - name: Lint with Ruff run: | python -m ruff check --output-format=github . @@ -87,7 +87,12 @@ jobs: sccache: 'true' manylinux: auto working-directory: bindings/python - - name: Run tests + - name: Prepare tests run: | python -m pip install openchecks --find-links target/wheels + - name: Run tests + run: | python -m pytest --hypothesis-profile default -n=auto tests/ + - name: Run doc tests + run: | + python -m sphinx --builder=doctest docs/source docs/build diff --git a/bindings/python/src/check.rs b/bindings/python/src/check.rs index 407ea3c..1698e5d 100644 --- a/bindings/python/src/check.rs +++ b/bindings/python/src/check.rs @@ -26,6 +26,10 @@ impl From for base_openchecks::CheckHint { #[pymethods] impl CheckHint { + /// The check supports no extra features. + /// + /// This should be considered the most conservative check *feature*. For + /// example, no auto-fix, check cannot be skipped before running, etc. #[classattr] #[allow(non_snake_case)] pub(crate) fn NONE() -> Self { @@ -143,6 +147,21 @@ impl CheckHintIterator { } } +/// CheckMetadata() +/// +/// The check metadata. +/// +/// This stores the information about the check that is either useful for humans +/// (the :code:`title`) and :code:`description`) or useful for systems that uses +/// the check (:code:`hint`). For example, a user interface could use the title +/// and description to render information for an artist to inform them about +/// what the check will validate and how it will fix issues (if supported). The +/// hint then could be used to render other useful information such as whether +/// the check supports automatic fixes in general, whether it could be +/// overridden by a supervisor, etc. +/// +/// This should not be inherited directly. Use :code:`BaseCheck` or +/// :code:`AsyncBaseCheck` instead. #[pyclass(subclass)] pub(crate) struct CheckMetadata {} @@ -196,9 +215,86 @@ impl CheckMetadata { } } -/// BaseCheck +/// BaseCheck() +/// +/// The base check class to be inherited from. +/// +/// This is responsible for validating the input data and returning a result +/// such as pass or fail. It can also provide extra data such as what caused the +/// status (for example, the scene nodes that are named incorrectly). +/// +/// If the check supports it, then the data being validated can be automatically +/// fixed. +/// +/// Example: +/// +/// Simple Check +/// ------------ +/// +/// .. testsetup:: +/// +/// from openchecks import CheckResult, Item, BaseCheck, Status, run +/// +/// .. testcode:: +/// +/// class IsEvenCheck(BaseCheck): +/// def __init__(self, value: int) -> None: +/// self.__value = value +/// super().__init__() +/// +/// def title(self) -> str: +/// return "Is Even Check" +/// +/// def description(self) -> str: +/// return "Check if the number is even." +/// +/// def check(self) -> CheckResult: +/// if self.__value % 2 == 0: +/// return CheckResult.passed("Number is even.") +/// else: +/// return CheckResult.failed("Number is not even.") +/// +/// check = IsEvenCheck(2) +/// result = run(check) +/// assert result.status() == Status.Passed +/// +/// Check with Automatic Fix +/// ------------------------ +/// +/// .. testsetup:: +/// +/// from openchecks import CheckResult, Item, BaseCheck, Status, auto_fix, run +/// +/// .. testcode:: +/// +/// class IsZeroCheck(BaseCheck): +/// def __init__(self, value: int) -> None: +/// self.__value = value +/// super().__init__() +/// +/// def title(self) -> str: +/// return "Is Zero Check" +/// +/// def description(self) -> str: +/// return "Check if the number is zero." +/// +/// def check(self) -> CheckResult: +/// if self.__value == 0: +/// return CheckResult.passed("Number is zero.") +/// else: +/// return CheckResult.failed("Number is not zero.", can_fix=True) +/// +/// def auto_fix(self) -> None: +/// self.__value = 0 +/// +/// check = IsZeroCheck(1) +/// result = run(check) +/// assert result.status() == Status.Failed +/// +/// if result.can_fix(): +/// result = auto_fix(check) +/// assert result.status() == Status.Passed /// -/// The base check to subclass. #[pyclass(extends = CheckMetadata, subclass)] #[derive(Debug)] pub(crate) struct BaseCheck {} @@ -216,6 +312,9 @@ impl BaseCheck { /// Run a validation on the input data and output the result of the /// validation. /// + /// Raises: + /// NotImplementedError: The check has not been implemented. + /// /// Returns: /// CheckResult[T]: The result of the check. pub(crate) fn check(&self) -> PyResult { @@ -225,14 +324,102 @@ impl BaseCheck { /// auto_fix(self) /// /// Automatically fix the issue detected by the :code:`Check.check` method. + /// + /// Raises: + /// NotImplementedError: The automatic fix has not been implemented. pub(crate) fn auto_fix(&self) -> PyResult<()> { Err(PyNotImplementedError::new_err("auto_fix not implemented")) } } -/// AsyncBaseCheck +/// AsyncBaseCheck() +/// +/// The base check class to be inherited from for async code. +/// +/// This is responsible for validating the input data and returning a result +/// such as pass or fail. It can also provide extra data such as what caused the +/// status (for example, the scene nodes that are named incorrectly). +/// +/// If the check supports it, then the data being validated can be automatically +/// fixed. +/// +/// Example: +/// +/// Simple Check +/// ------------ +/// +/// .. testsetup:: +/// import asyncio +/// +/// from openchecks import CheckResult, Item, AsyncBaseCheck, Status, async_run +/// +/// .. testcode:: +/// +/// class IsEvenCheck(AsyncBaseCheck): +/// def __init__(self, value: int) -> None: +/// self.__value = value +/// super().__init__() +/// +/// def title(self) -> str: +/// return "Is Even Check" +/// +/// def description(self) -> str: +/// return "Check if the number is even." +/// +/// async def async_check(self) -> CheckResult: +/// if self.__value % 2 == 0: +/// return CheckResult.passed("Number is even.") +/// else: +/// return CheckResult.failed("Number is not even.") +/// +/// async def main(): +/// check = IsEvenCheck(2) +/// result = await async_run(check) +/// assert result.status() == Status.Passed +/// +/// asyncio.run(main()) +/// +/// Check with Automatic Fix +/// ------------------------ +/// +/// .. testsetup:: +/// +/// import asyncio +/// +/// from openchecks import CheckResult, Item, AsyncBaseCheck, Status, async_auto_fix, async_run +/// +/// .. testcode:: +/// +/// class IsZeroCheck(AsyncBaseCheck): +/// def __init__(self, value: int) -> None: +/// self.__value = value +/// super().__init__() +/// +/// def title(self) -> str: +/// return "Is Zero Check" +/// +/// def description(self) -> str: +/// return "Check if the number is zero." +/// +/// async def async_check(self) -> CheckResult: +/// if self.__value == 0: +/// return CheckResult.passed("Number is zero.") +/// else: +/// return CheckResult.failed("Number is not zero.", can_fix=True) +/// +/// async def async_auto_fix(self) -> None: +/// self.__value = 0 +/// +/// async def main(): +/// check = IsZeroCheck(1) +/// result = await async_run(check) +/// assert result.status() == Status.Failed +/// +/// if result.can_fix(): +/// result = await async_auto_fix(check) +/// assert result.status() == Status.Passed /// -/// The base check to subclass. +/// asyncio.run(main()) #[pyclass(extends = CheckMetadata, subclass)] #[derive(Debug)] pub(crate) struct AsyncBaseCheck {} diff --git a/bindings/python/src/item.rs b/bindings/python/src/item.rs index 4221167..67c874e 100644 --- a/bindings/python/src/item.rs +++ b/bindings/python/src/item.rs @@ -1,5 +1,50 @@ use pyo3::{intern, prelude::*, types::PyString}; +/// Item(value: T, type_hint: Optional[str] = None) -> None +/// +/// The item is a wrapper to make a result item more user interface friendly. +/// +/// Result items represent the objects that caused a result. For example, if a +/// check failed because the bones in a character rig are not properly named, +/// then the items would contain the bones that are named incorrectly. +/// +/// The item wrapper makes the use of items user interface friendly because it +/// implements item sorting and a string representation of the item. +/// +/// Example: +/// +/// .. testsetup:: +/// +/// from __future__ import annotations +/// +/// from openchecks import Item +/// +/// class SceneNode: +/// def __init__(self, name): +/// self.__name = name +/// +/// def name(self): +/// return self.__name +/// +/// .. testcode:: +/// +/// class SceneItem(Item): +/// def __str__(self) -> str: +/// return self.value().name() +/// +/// def __eq__(self, other: SceneItem) -> bool: +/// return self.value().name() == other.value().name() +/// +/// def __lt__(self, other: SceneItem) -> bool: +/// return self.value().name() < other.value().name() +/// +/// a = SceneItem(SceneNode("a")) +/// b = SceneItem(SceneNode("b")) +/// +/// assert a != b +/// assert a < b +/// assert str(a) == "a" +/// #[pyclass(subclass)] #[derive(Debug, Clone)] pub(crate) struct Item { @@ -82,10 +127,21 @@ impl Item { } } + /// value(self) -> T + /// + /// The wrapped value fn value<'py>(&'py self, py: Python<'py>) -> PyResult<&'py PyAny> { Ok(self.value.as_ref(py)) } + /// type_hint(self) -> Optional[str] + /// + /// A type hint can be used to add a hint to a system that the given type + /// represents something else. For example, the value could be a string, but + /// this is a scene path. + /// + /// A user interface could use this hint to select the item in the + /// application. fn type_hint(&self) -> Option<&str> { match &self.type_hint { Some(type_hint) => Some(type_hint.as_str()), diff --git a/bindings/python/src/runner.rs b/bindings/python/src/runner.rs index 27c1af7..3ab71b2 100644 --- a/bindings/python/src/runner.rs +++ b/bindings/python/src/runner.rs @@ -2,6 +2,42 @@ use pyo3::prelude::*; use crate::{check_wrapper::AsyncCheckWrapper, check_wrapper::CheckWrapper, result::CheckResult}; +/// Run a check. +/// +/// Running a check should never fail, but instead return a failure check +/// result. The run function might return a :code:`Status.SystemError` if the +/// system runs into an error that must be resolved by the team supporting and +/// implementing the checks. +/// +/// Example: +/// +/// .. testsetup:: +/// +/// from openchecks import CheckResult, Item, BaseCheck, Status, run +/// +/// class IsEvenCheck(BaseCheck): +/// def __init__(self, value: int) -> None: +/// self.__value = value +/// super().__init__() +/// +/// def title(self) -> str: +/// return "Is Even Check" +/// +/// def description(self) -> str: +/// return "Check if the number is even." +/// +/// def check(self) -> CheckResult: +/// if self.__value % 2 == 0: +/// return CheckResult.passed("Number is even.") +/// else: +/// return CheckResult.failed("Number is not even.") +/// +/// .. testcode:: +/// +/// check = IsEvenCheck(2) +/// result = run(check) +/// assert result.status() == Status.Passed +/// #[pyfunction] pub(crate) fn run(py: Python<'_>, check: PyObject) -> PyResult { if !check.as_ref(py).is_instance_of::() { @@ -22,6 +58,54 @@ pub(crate) fn run(py: Python<'_>, check: PyObject) -> PyResult { Ok(result.into()) } +/// Automatically fix an issue found by a check. +/// +/// This function should only be run after the :code:`run` returns a result, and +/// that result can be fixed. Otherwise, the fix might try to fix an already +/// "good" object, causing issues with the object. +/// +/// The auto-fix will re-run the :code:`run` to validate that it has actually +/// fixed the issue. +/// +/// This will return a result with the +/// :code:`Status.SystemError` status if the check does +/// not have the :code:`CheckHint.AUTO_FIX` flag set, or +/// an auto-fix returned an error. In the case of the latter, it will include +/// the error with the check result. +/// +/// .. testsetup:: +/// +/// from openchecks import CheckResult, Item, BaseCheck, Status, auto_fix, run +/// +/// class IsZeroCheck(BaseCheck): +/// def __init__(self, value: int) -> None: +/// self.__value = value +/// super().__init__() +/// +/// def title(self) -> str: +/// return "Is Zero Check" +/// +/// def description(self) -> str: +/// return "Check if the number is zero." +/// +/// def check(self) -> CheckResult: +/// if self.__value == 0: +/// return CheckResult.passed("Number is zero.") +/// else: +/// return CheckResult.failed("Number is not zero.", can_fix=True) +/// +/// def auto_fix(self) -> None: +/// self.__value = 0 +/// +/// .. testcode:: +/// +/// check = IsZeroCheck(1) +/// result = run(check) +/// assert result.status() == Status.Failed +/// +/// if result.can_fix(): +/// result = auto_fix(check) +/// assert result.status() == Status.Passed #[pyfunction] pub(crate) fn auto_fix(py: Python<'_>, check: PyObject) -> PyResult { if !check.as_ref(py).is_instance_of::() { @@ -41,6 +125,47 @@ pub(crate) fn auto_fix(py: Python<'_>, check: PyObject) -> PyResult Ok(result.into()) } +/// Run a check in an async context. +/// +/// Running a check should never fail, but instead return a failure check +/// result. The run function might return a :code:`Status.SystemError` if the +/// system runs into an error that must be resolved by the team supporting and +/// implementing the checks. +/// +/// Example: +/// +/// .. testsetup:: +/// +/// import asyncio +/// +/// from openchecks import CheckResult, Item, AsyncBaseCheck, Status, async_run +/// +/// class IsEvenCheck(AsyncBaseCheck): +/// def __init__(self, value: int) -> None: +/// self.__value = value +/// super().__init__() +/// +/// def title(self) -> str: +/// return "Is Even Check" +/// +/// def description(self) -> str: +/// return "Check if the number is even." +/// +/// async def async_check(self) -> CheckResult: +/// if self.__value % 2 == 0: +/// return CheckResult.passed("Number is even.") +/// else: +/// return CheckResult.failed("Number is not even.") +/// +/// .. testcode:: +/// +/// async def main(): +/// check = IsEvenCheck(2) +/// result = run(check) +/// assert result.status() == Status.Passed +/// +/// asyncio.run(main()) +/// #[pyfunction] pub(crate) fn async_run(py: Python<'_>, check: PyObject) -> PyResult<&PyAny> { pyo3_asyncio::tokio::future_into_py(py, async move { @@ -75,6 +200,59 @@ pub(crate) fn async_run(py: Python<'_>, check: PyObject) -> PyResult<&PyAny> { }) } +/// Automatically fix an issue found by a check in an async context. +/// +/// This function should only be run after the :code:`async_run` returns a +/// result, and that result can be fixed. Otherwise, the fix might try to fix an +/// already "good" object, causing issues with the object. +/// +/// The auto-fix will re-run the :code:`async_run` to validate that it has +/// actually fixed the issue. +/// +/// This will return a result with the +/// :code:`Status.SystemError` status if the check does +/// not have the :code:`CheckHint.AUTO_FIX` flag set, or +/// an auto-fix returned an error. In the case of the latter, it will include +/// the error with the check result. +/// +/// .. testsetup:: +/// +/// import asyncio +/// +/// from openchecks import CheckResult, Item, AsyncBaseCheck, Status, async_auto_fix, async_run +/// +/// class IsZeroCheck(AsyncBaseCheck): +/// def __init__(self, value: int) -> None: +/// self.__value = value +/// super().__init__() +/// +/// def title(self) -> str: +/// return "Is Zero Check" +/// +/// def description(self) -> str: +/// return "Check if the number is zero." +/// +/// async def async_check(self) -> CheckResult: +/// if self.__value == 0: +/// return CheckResult.passed("Number is zero.") +/// else: +/// return CheckResult.failed("Number is not zero.", can_fix=True) +/// +/// async def async_auto_fix(self) -> None: +/// self.__value = 0 +/// +/// .. testcode:: +/// +/// async def main(): +/// check = IsZeroCheck(1) +/// result = run(check) +/// assert result.status() == Status.Failed +/// +/// if result.can_fix(): +/// result = auto_fix(check) +/// assert result.status() == Status.Passed +/// +/// asyncio.run(main()) #[pyfunction] pub(crate) fn async_auto_fix(py: Python<'_>, check: PyObject) -> PyResult<&PyAny> { pyo3_asyncio::tokio::future_into_py(py, async move { From 142d4becad65d2de261ee9f20b79ae655be9da8a Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 5 Aug 2024 22:10:50 -0700 Subject: [PATCH 4/8] Fix docs dependencies Python versions Signed-off-by: Scott Wilson --- bindings/python/pyproject.toml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 9b97157..fab773b 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -35,7 +35,13 @@ test = [ "pytest-xdist ~= 3.5", "ruff ~= 0.2", ] -docs = ["myst-parser ~= 3.0", "sphinx ~= 7.2", "sphinx-rtd-theme ~= 2.0"] +docs = [ + "myst-parser ~= 1.0; python_version == \"3.7\"", + "myst-parser ~= 3.0; python_version > \"3.7\"", + "sphinx ~= 5.3; python_version == \"3.7\"", + "sphinx ~= 7.2; python_version > \"3.7\"", + "sphinx-rtd-theme ~= 2.0", +] fuzz = ["openchecks[test]", "atheris ~= 2.3"] [tool.maturin] From 3b11db18446c788abf72677c2bf4129d59737e7e Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 5 Aug 2024 22:14:48 -0700 Subject: [PATCH 5/8] More docs deps dependencies fixes Signed-off-by: Scott Wilson --- bindings/python/pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index fab773b..3162270 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -39,7 +39,8 @@ docs = [ "myst-parser ~= 1.0; python_version == \"3.7\"", "myst-parser ~= 3.0; python_version > \"3.7\"", "sphinx ~= 5.3; python_version == \"3.7\"", - "sphinx ~= 7.2; python_version > \"3.7\"", + "sphinx ~= 6.2; python_version >= \"3.8\", < \"3.9\"", + "sphinx ~= 7.2; python_version > \"3.8\"", "sphinx-rtd-theme ~= 2.0", ] fuzz = ["openchecks[test]", "atheris ~= 2.3"] From 21536323136961674f14d154ea345bd505e1d981 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 5 Aug 2024 22:16:16 -0700 Subject: [PATCH 6/8] More docs deps fixes Signed-off-by: Scott Wilson --- bindings/python/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 3162270..817cd9d 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -39,7 +39,7 @@ docs = [ "myst-parser ~= 1.0; python_version == \"3.7\"", "myst-parser ~= 3.0; python_version > \"3.7\"", "sphinx ~= 5.3; python_version == \"3.7\"", - "sphinx ~= 6.2; python_version >= \"3.8\", < \"3.9\"", + "sphinx ~= 6.2; python_version >= \"3.8\" and < \"3.9\"", "sphinx ~= 7.2; python_version > \"3.8\"", "sphinx-rtd-theme ~= 2.0", ] From d4bb70d51747c7914b48c29fbe041c516aef43e2 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 5 Aug 2024 22:22:22 -0700 Subject: [PATCH 7/8] More docs deps fixes Signed-off-by: Scott Wilson --- bindings/python/pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 817cd9d..7c7ba76 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -39,8 +39,8 @@ docs = [ "myst-parser ~= 1.0; python_version == \"3.7\"", "myst-parser ~= 3.0; python_version > \"3.7\"", "sphinx ~= 5.3; python_version == \"3.7\"", - "sphinx ~= 6.2; python_version >= \"3.8\" and < \"3.9\"", - "sphinx ~= 7.2; python_version > \"3.8\"", + "sphinx ~= 6.2; python_version >= \"3.8\" and python_version < \"3.9\"", + "sphinx ~= 7.2; python_version >= \"3.9\"", "sphinx-rtd-theme ~= 2.0", ] fuzz = ["openchecks[test]", "atheris ~= 2.3"] From 246903548180fa8eac126d447e44c32916939f12 Mon Sep 17 00:00:00 2001 From: Scott Wilson Date: Mon, 5 Aug 2024 22:29:45 -0700 Subject: [PATCH 8/8] Change builder flag for sphinx tests Signed-off-by: Scott Wilson --- .github/workflows/test_suite_python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_suite_python.yml b/.github/workflows/test_suite_python.yml index 6994f38..5633d6e 100644 --- a/.github/workflows/test_suite_python.yml +++ b/.github/workflows/test_suite_python.yml @@ -95,4 +95,4 @@ jobs: python -m pytest --hypothesis-profile default -n=auto tests/ - name: Run doc tests run: | - python -m sphinx --builder=doctest docs/source docs/build + python -m sphinx -b=doctest docs/source docs/build