Skip to content

feat(checks): add operator composition and utility functions#2396

Open
mvanhorn wants to merge 2 commits intoGiskard-AI:mainfrom
mvanhorn:osc/2395-check-composition
Open

feat(checks): add operator composition and utility functions#2396
mvanhorn wants to merge 2 commits intoGiskard-AI:mainfrom
mvanhorn:osc/2395-check-composition

Conversation

@mvanhorn
Copy link
Copy Markdown

Summary

Adds ergonomic composition for checks via Python operators and utility functions, as requested in #2395.

Changes

Operator overloads on Check base class (core/check.py)

  • check_a & check_b creates AllOf(checks=[check_a, check_b]) (flattens nested AllOf)
  • check_a | check_b creates AnyOf(checks=[check_a, check_b]) (flattens nested AnyOf)
  • ~check_a creates Not(check=check_a)

Utility functions (builtin/composition.py)

  • all_of(*checks) - variadic AllOf constructor
  • any_of(*checks) - variadic AnyOf constructor
  • expect_fails(check) - readable alias for Not

Exports

  • All three utilities exported from giskard.checks

Before/After

# Before
AllOf(checks=[Equals(expected_value="x", key="k"), Not(check=Equals(expected_value="y", key="k"))])

# After (utilities)
all_of(Equals(expected_value="x", key="k"), expect_fails(Equals(expected_value="y", key="k")))

# After (operators)
Equals(expected_value="x", key="k") & ~Equals(expected_value="y", key="k")

The existing AllOf, AnyOf, Not classes and their serialization are unchanged. Operators and utilities produce the same model instances.

Closes #2395

This contribution was developed with AI assistance (Claude Code).

Adds & (AND), | (OR), ~ (NOT) operators to the Check base class, plus
all_of(), any_of(), and expect_fails() utility functions for more
ergonomic check composition.

Before:
  AllOf(checks=[Equals(...), Not(check=Equals(...))])

After:
  all_of(Equals(...), expect_fails(Equals(...)))
  # or with operators:
  Equals(...) & ~Equals(...)

Closes Giskard-AI#2395
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces composition utilities for checks, adding functional helpers like all_of, any_of, and expect_fails, along with operator overloading (&, |, ~) for the base Check class to allow for more intuitive check combinations. The review feedback recommends replacing string-based class name comparisons with isinstance() checks in the __and__ and __or__ methods to improve code robustness and better support static analysis tools.

Comment on lines +48 to +56
from ..builtin.composition import AllOf

my_checks: list[Check] = (
list(self.checks) if hasattr(self, "checks") and self.__class__.__name__ == "AllOf" else [self]
)
other_checks: list[Check] = (
list(other.checks) if hasattr(other, "checks") and other.__class__.__name__ == "AllOf" else [other]
)
return AllOf(checks=my_checks + other_checks)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using isinstance() is preferred over comparing __class__.__name__ strings. It is more robust, idiomatic (PEP 8), and allows static analysis tools to correctly narrow the types of self and other. This ensures that the checks attribute is recognized by linters and avoids potential NameError issues with the Check type hint inside the class definition.

Suggested change
from ..builtin.composition import AllOf
my_checks: list[Check] = (
list(self.checks) if hasattr(self, "checks") and self.__class__.__name__ == "AllOf" else [self]
)
other_checks: list[Check] = (
list(other.checks) if hasattr(other, "checks") and other.__class__.__name__ == "AllOf" else [other]
)
return AllOf(checks=my_checks + other_checks)
from ..builtin.composition import AllOf
my_checks = list(self.checks) if isinstance(self, AllOf) else [self]
other_checks = list(other.checks) if isinstance(other, AllOf) else [other]
return AllOf(checks=my_checks + other_checks)
Comment on lines +65 to +73
from ..builtin.composition import AnyOf

my_checks: list[Check] = (
list(self.checks) if hasattr(self, "checks") and self.__class__.__name__ == "AnyOf" else [self]
)
other_checks: list[Check] = (
list(other.checks) if hasattr(other, "checks") and other.__class__.__name__ == "AnyOf" else [other]
)
return AnyOf(checks=my_checks + other_checks)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using isinstance() is preferred over comparing __class__.__name__ strings. It is more robust, idiomatic (PEP 8), and allows static analysis tools to correctly narrow the types of self and other. This ensures that the checks attribute is recognized by linters and avoids potential NameError issues with the Check type hint inside the class definition.

Suggested change
from ..builtin.composition import AnyOf
my_checks: list[Check] = (
list(self.checks) if hasattr(self, "checks") and self.__class__.__name__ == "AnyOf" else [self]
)
other_checks: list[Check] = (
list(other.checks) if hasattr(other, "checks") and other.__class__.__name__ == "AnyOf" else [other]
)
return AnyOf(checks=my_checks + other_checks)
from ..builtin.composition import AnyOf
my_checks = list(self.checks) if isinstance(self, AnyOf) else [self]
other_checks = list(other.checks) if isinstance(other, AnyOf) else [other]
return AnyOf(checks=my_checks + other_checks)
Addresses gemini-code-assist review on PR Giskard-AI#2396. isinstance() is
idiomatic (PEP 8), enables static type narrowing, and the AllOf/AnyOf
imports are already in scope inside the methods.

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
@mvanhorn
Copy link
Copy Markdown
Author

Applied in 730102b - swapped both __class__.__name__ checks for isinstance(). AllOf/AnyOf are already imported inside the methods, so no scope change needed. The redundant hasattr(self, "checks") guard is also gone since isinstance(x, AllOf) already implies .checks exists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

1 participant