Skip to content
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
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

- Added new validator, ``require_not_one_of``, that checks that value is not contained
in a forbidden set of values [#15](https://github.com/mcflugen/requireit/issues/15)
- Added new validators that check if an object's length is exactly, at most, or at least
a given value [#16](https://github.com/mcflugen/requireit/issues/16)

## 0.2.0 (2026-01-16)

Expand Down
95 changes: 95 additions & 0 deletions src/requireit.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,3 +486,98 @@ def require_less_than(
raise ValidationError(f"{name} must be {op} {upper_str}")

return value


def _length_of(value, *, name: str | None):
name = name or "value"
try:
return len(value)
except TypeError as err:
raise ValidationError(f"{name} must have a length") from err


def require_length_is(value: Any, length: int, *, name: str | None = None):
"""Require a value to have an exact length.

Parameters
----------
value : any
Object whose length will be validated.
length : int
Required length of the object.
name : str, optional
Name used in error messages.

Returns
-------
value
The validated object.

Raises
------
ValidationError
If the object does not have the required length or has no length.
"""
name = name or "value"

if _length_of(value, name=name) != length:
raise ValidationError(f"{name} must have length {length}")
return value


def require_length_is_at_least(value: Any, length: int, *, name: str | None = None):
"""Require an object to have a minimum length.

Parameters
----------
value : and
Object whose length will be validated.
length : int
Minimum allowed length.
name : str, optional
Name used in error messages.

Returns
-------
value
The validated object.

Raises
------
ValidationError
If the object's length is less than `length` or has no length.
"""
name = name or "value"

if _length_of(value, name=name) < length:
raise ValidationError(f"{name} must have length >= {length}")
return value


def require_length_is_at_most(value: Any, length: int, *, name: str | None = None):
"""Require an object to have a maximum length.

Parameters
----------
value : any
Object whose length will be validated.
length : int
Maximum allowed length.
name : str, optional
Name used in error messages.

Returns
-------
value
The validated object.

Raises
------
ValidationError
If the object's length exceeds `length` or has no length.
"""
name = name or "value"

if _length_of(value, name=name) > length:
raise ValidationError(f"{name} must have length <= {length}")
return value
40 changes: 40 additions & 0 deletions tests/requireit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
from requireit import ValidationError
from requireit import require_array
from requireit import require_between
from requireit import require_length_is
from requireit import require_length_is_at_least
from requireit import require_length_is_at_most
from requireit import require_less_than
from requireit import require_negative
from requireit import require_nonnegative
Expand All @@ -23,6 +26,13 @@
(
pytest.param(partial(require_between, -1, 0, 1), id="between-below"),
pytest.param(partial(require_between, 2, 0, 1), id="between-above"),
pytest.param(partial(require_length_is, [1, 2, 3], 2), id="length_is-2"),
pytest.param(
partial(require_length_is_at_least, [1, 2, 3], 4), id="length_is_at_least-4"
),
pytest.param(
partial(require_length_is_at_most, [1, 2, 3], 2), id="length_is_at_most-2"
),
pytest.param(partial(require_negative, 0), id="negative-0"),
pytest.param(partial(require_nonnegative, -1), id="nonnegative--1"),
pytest.param(partial(require_nonpositive, 1), id="nonpositive-1"),
Expand Down Expand Up @@ -339,3 +349,33 @@ def test_require_less_than_inclusive(value):
def test_require_less_than_error_message(inclusive, op):
with pytest.raises(ValidationError, match=f"^value must be {op} "):
require_less_than(2.0, upper=1.0, inclusive=inclusive)


@pytest.mark.parametrize(
"value", ({1, 2, 3}, [4, 5, 6], "foo", {"0": 0, "1": 1, "2": 2})
)
def test_require_length_is_ok(value):
actual = require_length_is(value, 3)
assert actual is value
actual = require_length_is_at_least(value, 2)
assert actual is value
actual = require_length_is_at_most(value, 4)
assert actual is value


@pytest.mark.parametrize(
"value", ({1, 2, 3}, [4, 5, 6], "foo", {"0": 0, "1": 1, "2": 2})
)
def test_require_length_is_not_ok(value):
with pytest.raises(ValidationError, match="value must have length"):
require_length_is(value, 2)
with pytest.raises(ValidationError, match="value must have length >="):
require_length_is_at_least(value, 4)
with pytest.raises(ValidationError, match="value must have length <="):
require_length_is_at_most(value, 2)


@pytest.mark.parametrize("value", (0, True, 3.14, 1 + 2j, None))
def test_require_length_without_len(value):
with pytest.raises(ValidationError, match="value must have a length"):
require_length_is(value, 2)