From 1b3b575172776c776564f54a3c11fb8e0492829a Mon Sep 17 00:00:00 2001 From: mcflugen Date: Thu, 5 Feb 2026 17:43:47 -0700 Subject: [PATCH 1/3] add require_length_* validators --- src/requireit.py | 95 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/src/requireit.py b/src/requireit.py index b9ec547..2f42188 100644 --- a/src/requireit.py +++ b/src/requireit.py @@ -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 From 625009d93c49f1694d8e87441c1b12912fdb2956 Mon Sep 17 00:00:00 2001 From: mcflugen Date: Thu, 5 Feb 2026 19:03:13 -0700 Subject: [PATCH 2/3] add unit tests for require_length validators --- tests/requireit_test.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/requireit_test.py b/tests/requireit_test.py index c25c30a..b2f7a1f 100644 --- a/tests/requireit_test.py +++ b/tests/requireit_test.py @@ -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 @@ -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"), @@ -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) From 0472eda69a78049d3e6f292e2f89a661d8045aef Mon Sep 17 00:00:00 2001 From: mcflugen Date: Thu, 5 Feb 2026 19:05:04 -0700 Subject: [PATCH 3/3] add an entry to the changelog --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 4d7ad6b..04da86c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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)