feat(checks): add operator composition and utility functions#2396
feat(checks): add operator composition and utility functions#2396mvanhorn wants to merge 2 commits intoGiskard-AI:mainfrom
Conversation
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
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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.
| 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) |
| 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) |
There was a problem hiding this comment.
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.
| 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>
|
Applied in 730102b - swapped both |
Summary
Adds ergonomic composition for checks via Python operators and utility functions, as requested in #2395.
Changes
Operator overloads on
Checkbase class (core/check.py)check_a & check_bcreatesAllOf(checks=[check_a, check_b])(flattens nested AllOf)check_a | check_bcreatesAnyOf(checks=[check_a, check_b])(flattens nested AnyOf)~check_acreatesNot(check=check_a)Utility functions (
builtin/composition.py)all_of(*checks)- variadic AllOf constructorany_of(*checks)- variadic AnyOf constructorexpect_fails(check)- readable alias for NotExports
giskard.checksBefore/After
The existing
AllOf,AnyOf,Notclasses and their serialization are unchanged. Operators and utilities produce the same model instances.Closes #2395
This contribution was developed with AI assistance (Claude Code).