Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for a discovery registry #329

Merged
merged 1 commit into from
Dec 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion bindings/python/openchecks.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import enum
from typing import TYPE_CHECKING, Generic, TypeVar

if TYPE_CHECKING:
from typing import List, Optional
from typing import Callable, List, Optional

T = TypeVar("T")

Expand Down Expand Up @@ -106,6 +106,16 @@ class AsyncBaseCheck(CheckMetadata, Generic[T]):

class CheckError(Exception): ...

class DiscoveryRegistry(Generic[T]):
def register(
self, query: Callable[[T], bool], generator: Callable[[T], list[BaseCheck]]
) -> None: ...
def register_async(
self, query: Callable[[T], bool], generator: Callable[[T], list[AsyncBaseCheck]]
) -> None: ...
def gather(self, context: T) -> Optional[list[BaseCheck]]: ...
def gather_async(self, context: T) -> Optional[list[AsyncBaseCheck]]: ...

def run(check: BaseCheck[T]) -> CheckResult[T]: ...
def auto_fix(check: BaseCheck[T]) -> CheckResult[T]: ...
async def async_run(check: AsyncBaseCheck[T]) -> CheckResult[T]: ...
Expand Down
22 changes: 0 additions & 22 deletions bindings/python/src/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,6 @@ impl CheckHint {
self.inner.contains(other.inner)
}

/// all() -> CheckHint
///
/// All of the check hint flags.
///
/// Returns:
Expand Down Expand Up @@ -143,8 +141,6 @@ impl CheckHintIterator {
}
}

/// CheckMetadata()
///
/// The check metadata.
///
/// This stores the information about the check that is either useful for humans
Expand Down Expand Up @@ -172,8 +168,6 @@ impl CheckMetadata {
Self {}
}

/// title(self) -> str
///
/// The human readable title for the check.
///
/// User interfaces should use the title for displaying the check.
Expand All @@ -184,8 +178,6 @@ impl CheckMetadata {
Err(PyNotImplementedError::new_err("title not implemented"))
}

/// description(self) -> str
///
/// The human readable description for the check.
///
/// This should include information about what the check is looking for,
Expand All @@ -200,8 +192,6 @@ impl CheckMetadata {
))
}

/// hint(self) -> CheckHint
///
/// The hint gives information about what features the check supports.
///
/// Returns:
Expand All @@ -211,8 +201,6 @@ impl CheckMetadata {
}
}

/// BaseCheck()
///
/// The base check class to be inherited from.
///
/// This is responsible for validating the input data and returning a result
Expand Down Expand Up @@ -306,8 +294,6 @@ impl BaseCheck {
(Self {}, CheckMetadata::new(args, kwargs))
}

/// check(self) -> CheckResult[T]
///
/// Run a validation on the input data and output the result of the
/// validation.
///
Expand All @@ -320,8 +306,6 @@ impl BaseCheck {
Err(PyNotImplementedError::new_err("check not implemented"))
}

/// auto_fix(self)
///
/// Automatically fix the issue detected by the :code:`Check.check` method.
///
/// Raises:
Expand All @@ -331,8 +315,6 @@ impl BaseCheck {
}
}

/// AsyncBaseCheck()
///
/// The base check class to be inherited from for async code.
///
/// This is responsible for validating the input data and returning a result
Expand Down Expand Up @@ -434,8 +416,6 @@ impl AsyncBaseCheck {
(Self {}, CheckMetadata::new(args, kwargs))
}

/// async_check(self) -> CheckResult[T]
///
/// Run a validation on the input data and output the result of the
/// validation.
///
Expand All @@ -447,8 +427,6 @@ impl AsyncBaseCheck {
})
}

/// async_auto_fix(self)
///
/// Automatically fix the issue detected by the :code:`AsyncCheck.async_check` method.
pub(crate) fn async_auto_fix<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
pyo3_async_runtimes::tokio::future_into_py::<_, ()>(py, async {
Expand Down
80 changes: 80 additions & 0 deletions bindings/python/src/discovery_registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use pyo3::prelude::*;

/// The discovery registry allows checks to be discovered based on the input
/// context.
///
/// The registry accepts two functions. The query function that is responsible
/// for querying if the context is valid, and the generate function that will
/// take the context and transform it into checks to be validated against.
#[pyclass]
pub(crate) struct DiscoveryRegistry {
plugins: Vec<(PyObject, PyObject)>,
async_plugins: Vec<(PyObject, PyObject)>,
}

#[pymethods]
impl DiscoveryRegistry {
/// Create a new instance of the discovery registry.
#[new]
fn new() -> Self {
Self {
plugins: Vec::new(),
async_plugins: Vec::new(),
}
}

/// Register the functions that will find the checks to be run.
///
/// The query function is responsible for querying if the gather method for
/// the registry will return the contents of the generator function. The
/// generator function is responsible for returning a list of checks for the
/// given context.
pub fn register(&mut self, query: Bound<PyAny>, generator: Bound<PyAny>) {
self.plugins.push((query.unbind(), generator.unbind()));
}

/// Register the functions that will find the checks to be run in async.
///
/// The query function is responsible for querying if the gather method for
/// the registry will return the contents of the generator function. The
/// generator function is responsible for returning a list of checks for the
/// given context.
pub fn register_async(&mut self, query: Bound<PyAny>, generator: Bound<PyAny>) {
self.async_plugins
.push((query.unbind(), generator.unbind()));
}

/// Return the checks that should be run for the given context.
///
/// If the result is `None`, then nothing was found that will return valid
/// checks.
///
/// If two query functions were to return a valid set of checks, then the
/// first one that was registered will return the associated checks.
pub fn gather(&self, py: Python, context: Bound<PyAny>) -> PyResult<Option<PyObject>> {
for (query, generator) in &self.plugins {
if query.call1(py, (&context,))?.is_truthy(py)? {
return Ok(Some(generator.call1(py, (context,))?));
}
}

Ok(None)
}

/// Return the async checks that should be run for the given context.
///
/// If the result is `None`, then nothing was found that will return valid
/// checks.
///
/// If two query functions were to return a valid set of checks, then the
/// first one that was registered will return the associated checks.
pub fn gather_async(&self, py: Python, context: Bound<PyAny>) -> PyResult<Option<PyObject>> {
for (query, generator) in &self.async_plugins {
if query.call1(py, (&context,))?.is_truthy(py)? {
return Ok(Some(generator.call1(py, (context,))?));
}
}

Ok(None)
}
}
8 changes: 1 addition & 7 deletions bindings/python/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ use std::sync::Arc;

use pyo3::{intern, prelude::*, types::PyString, BoundObject};

/// 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
Expand Down Expand Up @@ -168,15 +166,11 @@ impl Item {
}
}

/// value(self) -> T
///
/// The wrapped value
fn value<'py>(&'py self, py: Python<'py>) -> PyResult<&Bound<'py, PyAny>> {
fn value<'py>(&'py self, py: Python<'py>) -> PyResult<&'py Bound<'py, PyAny>> {
Ok(self.value.bind(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.
Expand Down
3 changes: 3 additions & 0 deletions bindings/python/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use discovery_registry::DiscoveryRegistry;
use pyo3::prelude::*;

mod check;
mod check_wrapper;
mod discovery_registry;
mod error;
mod item;
mod item_wrapper;
Expand All @@ -28,6 +30,7 @@ fn openchecks(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<CheckHint>()?;
m.add_class::<CheckMetadata>()?;
m.add_class::<CheckResult>()?;
m.add_class::<DiscoveryRegistry>()?;
m.add_class::<Item>()?;
m.add_class::<Status>()?;
m.add("CheckError", py.get_type::<CheckError>())?;
Expand Down
26 changes: 0 additions & 26 deletions bindings/python/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ use crate::{Item, Status};
use pyo3::exceptions::PyBaseException;
use pyo3::prelude::*;

/// CheckResult(status: Status, message: str, items: Optional[List[Item[T]]] = None, can_fix: bool = False, can_skip: bool = False, error: Optional[BaseException] = None)
///
/// A check result contains all of the information needed to know the status of
/// a check.
///
Expand Down Expand Up @@ -96,8 +94,6 @@ impl CheckResult {
Ok(Self { inner })
}

/// passed(message: str, items: Optional[List[Item[T]]], can_fix: bool, can_skip: bool) -> CheckResult
///
/// Create a new result that passed a check.
///
/// Args:
Expand Down Expand Up @@ -135,8 +131,6 @@ impl CheckResult {
Ok(Self { inner })
}

/// skipped(message: str, items: Optional[List[Item[T]]], can_fix: bool, can_skip: bool) -> CheckResult
///
/// Create a new result that skipped a check.
///
/// Args:
Expand Down Expand Up @@ -174,8 +168,6 @@ impl CheckResult {
Ok(Self { inner })
}

/// warning(message: str, items: Optional[List[Item[T]]], can_fix: bool, can_skip: bool) -> CheckResult
///
/// Create a new result that passed a check, but with a warning.
///
/// Warnings should be considered as passes, but with notes saying that
Expand Down Expand Up @@ -218,8 +210,6 @@ impl CheckResult {
Ok(Self { inner })
}

/// failed(message: str, items: Optional[List[Item[T]]], can_fix: bool, can_skip: bool) -> CheckResult
///
/// Create a new result that failed a check.
///
/// Failed checks in a validation system should not let the following
Expand Down Expand Up @@ -261,8 +251,6 @@ impl CheckResult {
Ok(Self { inner })
}

/// status(self) -> Status
///
/// The status of the result.
///
/// Returns:
Expand All @@ -271,8 +259,6 @@ impl CheckResult {
(*self.inner.status()).into()
}

/// message(self) -> str
///
/// A human readable message for the result.
///
/// If a check has issues, then this should include information about what
Expand All @@ -284,8 +270,6 @@ impl CheckResult {
self.inner.message()
}

/// items(self) -> Optional[List[Item[T]]]
///
/// The items that caused the result.
///
/// Returns:
Expand All @@ -299,8 +283,6 @@ impl CheckResult {
})
}

/// can_fix(self) -> bool
///
/// Whether the result can be fixed or not.
///
/// If the status is :code:`Status.SystemError`, then the check can
Expand All @@ -312,8 +294,6 @@ impl CheckResult {
self.inner.can_fix()
}

/// can_skip(self) -> bool
///
/// Whether the result can be skipped or not.
///
/// A result should only be skipped if the company decides that letting the
Expand All @@ -328,8 +308,6 @@ impl CheckResult {
self.inner.can_skip()
}

/// error(self) -> Optional[BaseException]
///
/// The error that caused the result.
///
/// This only really applies to the
Expand All @@ -345,8 +323,6 @@ impl CheckResult {
.map(|err| CheckError::new_err(err.to_string()))
}

/// check_duration(self) -> float
///
/// The duration of a check.
///
/// This is not settable outside of the check runner. It can be exposed to a
Expand All @@ -359,8 +335,6 @@ impl CheckResult {
self.inner.check_duration().as_secs_f64()
}

/// fix_duration(self) -> float
///
/// The duration of an auto-fix.
///
/// This is not settable outside of the auto-fix runner. It can be exposed
Expand Down
6 changes: 0 additions & 6 deletions bindings/python/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@ impl std::convert::From<Status> for base_openchecks::Status {

#[pymethods]
impl Status {
/// is_pending(self) -> bool
///
/// Return if a check is waiting to be run.
///
/// Returns:
Expand All @@ -88,8 +86,6 @@ impl Status {
status.is_pending()
}

/// has_passed(self) -> bool
///
/// Return if a check has passed.
///
/// Returns:
Expand All @@ -99,8 +95,6 @@ impl Status {
status.has_passed()
}

/// has_failed(self) -> bool
///
/// Return if a check has failed.
///
/// Returns:
Expand Down
Loading
Loading