"""Validator for iterable of specified type(s)."""
from typing import Any, Iterable, TypeVar, overload
from simplebench.exceptions import ErrorTag, SimpleBenchTypeError, SimpleBenchValueError
from simplebench.validators.exceptions.validators import _ValidatorsErrorTag
T = TypeVar("T")
@overload
def validate_iterable_of_type(
value: Any,
types: type[T],
field_name: str,
type_tag: ErrorTag,
value_tag: ErrorTag,
*,
allow_empty: bool = True,
exact_type: bool = False) -> list[T]: ...
@overload
def validate_iterable_of_type(
value: Any,
types: tuple[type, ...],
field_name: str,
type_tag: ErrorTag,
value_tag: ErrorTag,
*,
allow_empty: bool = True,
exact_type: bool = False) -> list[Any]: ...
[docs]
def validate_iterable_of_type(
value: Any,
types: type[T] | tuple[type, ...],
field_name: str,
type_tag: ErrorTag,
value_tag: ErrorTag,
*,
allow_empty: bool = True,
exact_type: bool = False) -> list[T] | list[Any]:
"""Validate that a value is an iterable of specified type(s).
When a single type is provided, the return type is automatically inferred as list[T].
When multiple types are provided, the return type is list[Any], which allows the
caller to narrow the type with an explicit annotation.
:param Iterable[Any] value: The iterable of values to validate.
:param types: A single type or tuple of allowed types.
:type types: type[T] | tuple[type, ...]
:param str field_name: The name of the field being validated (for error messages).
:param ErrorTag type_tag: The error tag to use for type errors.
:param ErrorTag value_tag: The error tag to use for value errors.
:param bool allow_empty: Whether to allow an empty iterable. Defaults to True.
:param bool exact_type: If set to True, require that the types be exactly the types specified, not subclasses.
Otherwise allow subclass matches.
:return: For single type, returns list[T]. For multiple types,
returns list[Any] which can be narrowed with explicit type annotation.
:rtype: list[T] | list[Any]
:raises SimpleBenchTypeError: If the value is not an iterable or contains invalid types.
:raises SimpleBenchValueError: If the iterable is empty and allow_empty is False.
Examples:
Single type (automatic inference):
.. code-block:: python
names = validate_iterable_of_type(['Alice'], str, 'names', ...)
# Type: list[str]
Multiple types (manual narrowing):
.. code-block:: python
mixed: list[str | int] = validate_iterable_of_type(
['a', 1], (str, int), 'items', ...
)
# Type: list[str | int]
"""
if not isinstance(value, Iterable) or isinstance(value, str):
raise SimpleBenchTypeError(
f'Invalid {field_name} type: {type(value).__name__}. '
f'Must be an iterable (list, tuple, etc.).',
tag=type_tag
)
value = list(value) # Convert to list for length check and indexing
if isinstance(types, type):
pass
else:
if not all(isinstance(t, type) for t in types):
raise SimpleBenchTypeError(
f'Invalid types argument: {types}. Must be a type or tuple of types.',
tag=_ValidatorsErrorTag.VALIDATE_ITERABLE_OF_TYPE_INVALID_TYPES_ARG
)
if not isinstance(field_name, str):
raise SimpleBenchTypeError(
f'Invalid field_name argument type: {type(field_name)}. Must be a str.',
tag=_ValidatorsErrorTag.VALIDATE_ITERABLE_OF_TYPE_INVALID_FIELD_NAME_ARG_TYPE
)
if not isinstance(type_tag, ErrorTag):
raise SimpleBenchTypeError(
f'Invalid type_tag argument type: {type(type_tag)}. Must be an ErrorTag.',
tag=_ValidatorsErrorTag.VALIDATE_ITERABLE_OF_TYPE_INVALID_TYPE_TAG_TYPE
)
if not isinstance(value_tag, ErrorTag):
raise SimpleBenchTypeError(
f'Invalid value_tag argument type: {type(value_tag)}. Must be an ErrorTag.',
tag=_ValidatorsErrorTag.VALIDATE_ITERABLE_OF_TYPE_INVALID_VALUE_TAG_TYPE
)
if not isinstance(allow_empty, bool):
raise SimpleBenchTypeError(
f'Invalid allow_empty argument type: {type(allow_empty)}. Must be a bool.',
tag=_ValidatorsErrorTag.VALIDATE_ITERABLE_OF_TYPE_INVALID_ALLOW_EMPTY_ARG_TYPE
)
if not isinstance(exact_type, bool):
raise SimpleBenchTypeError(
f'Invalid exact_type argument type: {type(exact_type)}. Must be a bool.',
tag=_ValidatorsErrorTag.VALIDATE_ITERABLE_OF_TYPE_INVALID_EXACT_TYPE_ARG_TYPE
)
if len(value) == 0 and not allow_empty:
raise SimpleBenchValueError(
f'Invalid {field_name}: iterable cannot be empty.',
tag=value_tag
)
# Format type names for error messages
if isinstance(types, tuple):
type_names = ' or '.join(t.__name__ for t in types)
else:
type_names = types.__name__
result: list[Any] = []
types_set: set[type] = set(types) if isinstance(types, tuple) else {types}
for i, item in enumerate(value):
if exact_type: # exact type matches only
if type(item) not in types_set:
raise SimpleBenchTypeError(
f'Invalid {field_name} element at index {i}: {type(item).__name__}. '
f'Must be {type_names}.',
tag=type_tag)
elif not isinstance(item, types): # types and their subclasses match
raise SimpleBenchTypeError(
f'Invalid {field_name} element at index {i}: {type(item).__name__}. '
f'Must be {type_names} or subclasses of them.',
tag=type_tag
)
result.append(item)
return result