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

Guess we doin V now (a new validation framework) #459

Open
wants to merge 33 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
41687f2
Guess we doin V now
Tinche Nov 22, 2023
d554ef8
Conditional import
Tinche Nov 23, 2023
bf166d6
Fix import
Tinche Nov 23, 2023
0d3fe85
Why have nice things when we can not have them?
Tinche Nov 23, 2023
3e722a4
Test with no detailed validation
Tinche Nov 23, 2023
bb1cd92
A small benchmark, as a treat
Tinche Nov 23, 2023
dacd24b
Validator coverage
Tinche Nov 23, 2023
a016e78
Exclude protocols from coverage
Tinche Nov 23, 2023
ef206d2
Error handling coverage
Tinche Nov 23, 2023
6edb9eb
Improve coverage tweak
Tinche Nov 23, 2023
2fab457
Ignore assert_nevers for coverage
Tinche Nov 23, 2023
eada8ad
Set up mypy tests
Tinche Nov 24, 2023
e5f337b
Renaming also validates
Tinche Nov 24, 2023
0d6e695
Maybe fix tests?
Tinche Nov 24, 2023
5c5f9e6
`is_unique` validator
Tinche Nov 24, 2023
a7bffce
Fix type annotation
Tinche Nov 24, 2023
cb6118d
Clean up import
Tinche Nov 24, 2023
86fdd00
ignoring_none
Tinche Nov 25, 2023
dd61ea3
Add import for 3.8
Tinche Nov 25, 2023
fd057ad
More coverage
Tinche Nov 25, 2023
6e9ce20
Tests for all_elements_must
Tinche Nov 27, 2023
be9a406
Rename, more tests
Tinche Nov 27, 2023
0a85e4d
Remove unused import
Tinche Nov 27, 2023
ab5eb98
Relock
Tinche Jan 12, 2024
a23589b
Update lockfile
Tinche Jan 12, 2024
9a039cf
Introduce VAnnotation
Tinche Jan 23, 2024
07218d5
General purpose `ensure`
Tinche Feb 3, 2024
7405d20
Properly import Annotated
Tinche Feb 4, 2024
430d0e7
More fixes
Tinche Feb 4, 2024
0daf1c3
Fix tests
Tinche Feb 4, 2024
059d949
Work on dicts
Tinche Feb 6, 2024
44f1118
Fix merge
Tinche Feb 16, 2024
91b0367
Initial dataclass support, start of docs
Tinche Mar 17, 2024
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
Prev Previous commit
Next Next commit
is_unique validator
  • Loading branch information
Tinche committed Mar 16, 2024
commit 5c5f9e653b58c0bf9dc5ff4d4b00950de1c80b18
3 changes: 2 additions & 1 deletion src/cattrs/v/__init__.py
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
IterableValidationError,
)
from ._fluent import V, customize
from ._validators import between, greater_than, len_between
from ._validators import between, greater_than, is_unique, len_between

__all__ = [
"customize",
@@ -19,6 +19,7 @@
"between",
"greater_than",
"len_between",
"is_unique",
]


9 changes: 0 additions & 9 deletions src/cattrs/v/_fluent.py
Original file line number Diff line number Diff line change
@@ -108,15 +108,6 @@ def replace_with(self, value: T) -> VOmitted:
return VOmitted(self.attr)


def is_unique(val: Collection[Any]) -> None:
"""Ensure all elements in a collection are unique.

Takes a value that implements Collection.
"""
if len(val) != len(set(val)):
raise ValueError(f"Value ({val}) not unique")


def ignoring_none(*validators: Callable[[T], None]) -> Callable[[T | None], None]:
"""
A validator for (f.e.) strings cannot be applied to `str | None`, but it can
11 changes: 10 additions & 1 deletion src/cattrs/v/_validators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from typing import Callable, Protocol, Sized, TypeVar
from collections.abc import Hashable
from typing import Callable, Collection, Protocol, Sized, TypeVar

T = TypeVar("T")

@@ -43,3 +44,11 @@ def assert_len_between(val: Sized, _min: int = min, _max: int = max) -> None:
raise ValueError(f"length ({length}) not between {_min} and {_max}")

return assert_len_between


def is_unique(val: Collection[Hashable]) -> None:
"""Ensure all elements in a collection are unique."""
if (length := len(val)) != (unique_length := len(set(val))):
raise ValueError(
f"Collection ({length} elem(s)) not unique, only {unique_length} unique elem(s)"
)
38 changes: 37 additions & 1 deletion tests/v/test_validators.py
Original file line number Diff line number Diff line change
@@ -6,7 +6,15 @@

from cattrs import BaseConverter
from cattrs.errors import ClassValidationError
from cattrs.v import V, between, customize, greater_than, len_between, transform_error
from cattrs.v import (
V,
between,
customize,
greater_than,
is_unique,
len_between,
transform_error,
)


@define
@@ -104,3 +112,31 @@ def test_len_between(converter: BaseConverter):
converter.structure({"a": [1, 2]}, WithList)

assert repr(exc_info.value) == "ValueError('length (2) not between 1 and 2')"


def test_unique(converter: BaseConverter):
"""The `is_unique` validator works."""

@define
class A:
a: list[int]

customize(converter, A, V(f(A).a).ensure(is_unique))

assert converter.structure({"a": [1]}, A) == A([1])

if converter.detailed_validation:
with raises(ClassValidationError) as exc_info:
converter.structure({"a": [1, 1]}, A)

assert transform_error(exc_info.value) == [
"invalid value (Collection (2 elem(s)) not unique, only 1 unique elem(s)) @ $.a"
]
else:
with raises(ValueError) as exc_info:
converter.structure({"a": [1, 1]}, A)

assert (
repr(exc_info.value)
== "ValueError('Collection (2 elem(s)) not unique, only 1 unique elem(s)')"
)
28 changes: 27 additions & 1 deletion tests/v/test_validators_mypy.yml
Original file line number Diff line number Diff line change
@@ -35,4 +35,30 @@

c = Converter()

v.customize(c, A, v.V(fields(A).a).ensure(v.len_between(5, 10)))
v.customize(c, A, v.V(fields(A).a).ensure(v.len_between(5, 10)))

- case: unique
main: |
from attrs import define, fields
from cattrs import v, Converter

@define
class A:
a: list[int]

c = Converter()

v.customize(c, A, v.V(fields(A).a).ensure(v.is_unique))

- case: unique_error_not_hashable
main: |
from attrs import define, fields
from cattrs import v, Converter

@define
class A:
a: list[dict]

c = Converter()

v.customize(c, A, v.V(fields(A).a).ensure(v.is_unique)) # E: Argument 1 to "ensure" of "V" has incompatible type "Callable[[Collection[Hashable]], None]"; expected "Callable[[list[dict[Any, Any]]], bool | None]" [arg-type]