-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: ✨ add
CheckErrorMatcher
object (#972)
## Description This PR adds a `CheckErrorMatcher` object. This is/will be used to filter out unwanted errors easily. Example usage: ```python errors = exclude_errors( errors, [ CheckErrorMatcher(validator="required", json_path="data"), CheckErrorMatcher(validator="type", json_path="path", message="not of type 'array'"), ], ) ``` I'm going to use this to replace the `check_required` flag in Sprout's check functions. <!-- Select quick/in-depth as necessary --> This PR needs an in-depth review. ## Checklist - [x] Added or updated tests - [x] Updated documentation - [x] Ran `just run-all` --------- Co-authored-by: Luke W. Johnston <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Signe Kirk Brødbæk <[email protected]>
- Loading branch information
1 parent
cf98f16
commit 9e0899e
Showing
4 changed files
with
237 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
from dataclasses import dataclass | ||
|
||
from seedcase_sprout.core.checks.check_error import CheckError | ||
|
||
|
||
@dataclass | ||
class CheckErrorMatcher: | ||
"""A class that helps to filter `CheckError`s based on their attributes.""" | ||
|
||
message: str | None = None | ||
json_path: str | None = None | ||
validator: str | None = None | ||
|
||
def matches(self, error: CheckError) -> bool: | ||
"""Determines if this matcher matches the given `CheckError`. | ||
Args: | ||
error: The `CheckError` to match. | ||
Returns: | ||
If there was a match. | ||
""" | ||
return ( | ||
self.message_matches(error) | ||
and self.json_path_matches(error) | ||
and self.validator_matches(error) | ||
) | ||
|
||
def message_matches(self, error: CheckError) -> bool: | ||
"""Determines if this matcher matches the message of the given `CheckError`. | ||
Args: | ||
error: The `CheckError` to match. | ||
Returns: | ||
If there was a match. | ||
""" | ||
return self.message is None or self.message in error.message | ||
|
||
def json_path_matches(self, error: CheckError) -> bool: | ||
"""Determines if this matcher matches the `json_path` of the given `CheckError`. | ||
Matching on the full `json_path` and matching on the field name are supported. | ||
Args: | ||
error: The `CheckError` to match. | ||
Returns: | ||
If there was a match. | ||
""" | ||
if self.json_path is None: | ||
return True | ||
if self.json_path == "" or self.json_path.startswith("$"): | ||
return self.json_path == error.json_path | ||
return error.json_path.endswith(f".{self.json_path}") | ||
|
||
def validator_matches(self, error: CheckError) -> bool: | ||
"""Determines if this matcher matches the validator of the given `CheckError`. | ||
Args: | ||
error: The `CheckError` to match. | ||
Returns: | ||
If there was a match. | ||
""" | ||
return self.validator is None or self.validator == error.validator |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from seedcase_sprout.core.checks.check_error import CheckError | ||
from seedcase_sprout.core.checks.check_error_matcher import CheckErrorMatcher | ||
|
||
|
||
def exclude_matching_errors( | ||
errors: list[CheckError], matchers: list[CheckErrorMatcher] | ||
) -> list[CheckError]: | ||
"""Returns a new list of errors, with errors matched by any `matchers` filtered out. | ||
Args: | ||
errors: The errors to exclude matching errors from. | ||
matchers: The matches to exclude. | ||
Returns: | ||
A list of errors without any errors that matched. | ||
""" | ||
return [ | ||
error | ||
for error in errors | ||
if not any(matcher.matches(error) for matcher in matchers) | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
from pytest import mark | ||
|
||
from seedcase_sprout.core.checks.check_error import CheckError | ||
from seedcase_sprout.core.checks.check_error_matcher import CheckErrorMatcher | ||
|
||
|
||
@mark.parametrize( | ||
"message,matcher,expected", | ||
[ | ||
("", "", True), | ||
("Match!", None, True), | ||
("Match!", "", True), | ||
("Complete match", "Complete match", True), | ||
("Beginning matches", "Beginning", True), | ||
("End matches", "d matches", True), | ||
("", "No match", False), | ||
("No match", "something else", False), | ||
], | ||
) | ||
def test_matches_message(message, matcher, expected): | ||
"""Should match if the matcher's message is a substring of the error's message.""" | ||
assert ( | ||
CheckErrorMatcher(message=matcher).matches( | ||
CheckError(message=message, json_path="$.name", validator="") | ||
) | ||
is expected | ||
) | ||
|
||
|
||
@mark.parametrize( | ||
"json_path,matcher,expected", | ||
[ | ||
("$.match", None, True), | ||
("$.no.match", "", False), | ||
("", "", True), | ||
("$.match", "$.match", True), | ||
("$.no.match", "$.no", False), | ||
("$.match", "match", True), | ||
("$.no.match", "no", False), | ||
("$.no.match", "other", False), | ||
], | ||
) | ||
def test_matches_json_path(json_path, matcher, expected): | ||
"""Should match if the matcher's `json_path` matches the full `json_path` of the | ||
error or its field name.""" | ||
assert ( | ||
CheckErrorMatcher(json_path=matcher).matches( | ||
CheckError(message="Hello", json_path=json_path, validator="") | ||
) | ||
is expected | ||
) | ||
|
||
|
||
@mark.parametrize( | ||
"validator,matcher,expected", | ||
[ | ||
("match", None, True), | ||
("match", "match", True), | ||
("", "", True), | ||
("no-match", "match", False), | ||
("no-match", "", False), | ||
("", "no-match", False), | ||
], | ||
) | ||
def test_matches_validator(validator, matcher, expected): | ||
"""Should match if the matcher's validator is the same as the error's.""" | ||
assert ( | ||
CheckErrorMatcher(validator=matcher).matches( | ||
CheckError(message="Hello", json_path="", validator=validator) | ||
) | ||
is expected | ||
) | ||
|
||
|
||
@mark.parametrize( | ||
"message,json_path,validator,expected", | ||
[ | ||
("name' is a", "name", "required", True), | ||
("name' is a", "name", "no-match", False), | ||
("name' is a", "no.match", "required", False), | ||
("no match", "name", "required", False), | ||
("no match", "name", "no-match", False), | ||
("no match", "no.match", "no-match", False), | ||
], | ||
) | ||
def test_matches_on_all_fields(message, json_path, validator, expected): | ||
"""Should only match if all fields match.""" | ||
assert ( | ||
CheckErrorMatcher( | ||
message=message, json_path=json_path, validator=validator | ||
).matches( | ||
CheckError( | ||
message="'name' is a required property", | ||
json_path="$.name", | ||
validator="required", | ||
) | ||
) | ||
is expected | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
from seedcase_sprout.core.checks.check_error import CheckError | ||
from seedcase_sprout.core.checks.check_error_matcher import CheckErrorMatcher | ||
from seedcase_sprout.core.checks.exclude_matching_errors import exclude_matching_errors | ||
|
||
errors = [ | ||
CheckError("'path' is a required property", "$.path", "required"), | ||
CheckError("'name' is a required property", "$.name", "required"), | ||
CheckError("123 is not of type 'string'", "$.resources[0].name", "type"), | ||
CheckError("pattern 'xyz' doesn't match", "$.created", "pattern"), | ||
CheckError("pattern 'xyz' doesn't match", "$.version", "pattern"), | ||
] | ||
|
||
|
||
def test_empty_matchers_have_no_effect(): | ||
"""An empty list as a list of matchers should have no effect.""" | ||
assert exclude_matching_errors(errors, []) == errors | ||
|
||
|
||
def test_not_matching_matchers_have_no_effect(): | ||
"""If no matchers match, no errors should be excluded.""" | ||
assert ( | ||
exclude_matching_errors( | ||
errors, | ||
[ | ||
CheckErrorMatcher(validator="no-match"), | ||
CheckErrorMatcher( | ||
validator="required", json_path="path", message="no match!" | ||
), | ||
CheckErrorMatcher( | ||
validator="type", json_path="$no.match", message="123 is not" | ||
), | ||
], | ||
) | ||
== errors | ||
) | ||
|
||
|
||
def test_matched_errors_are_excluded(): | ||
"""If any matchers match, the error should be excluded.""" | ||
assert ( | ||
exclude_matching_errors( | ||
errors, | ||
[ | ||
CheckErrorMatcher(json_path="name", validator="required"), | ||
CheckErrorMatcher(validator="pattern"), | ||
CheckErrorMatcher(validator="type"), | ||
CheckErrorMatcher(message="type 'string'"), | ||
], | ||
) | ||
== errors[:1] | ||
) |