"""Input validation framework for Qt Framework.
This module provides a comprehensive validation system with built-in validators
for common use cases (email, numbers, paths, etc.) and support for custom
validators and multi-field form validation.
Example:
Basic validation with built-in validators::
from qtframework.utils.validation import (
RequiredValidator,
EmailValidator,
LengthValidator,
NumberValidator,
ValidationError,
)
# Simple field validation
email_validator = EmailValidator()
try:
email_validator.validate("user@example.com", "email")
print("Valid email!")
except ValidationError as e:
print(f"Error: {e.message}")
# Chained validators
from qtframework.utils.validation import ValidatorChain
username_validators = ValidatorChain([
RequiredValidator("Username is required"),
LengthValidator(min_length=3, max_length=20),
])
result = username_validators.validate("ab", "username")
if not result.is_valid:
print(result.get_error_messages())
Complete form validation example::
from qtframework.utils.validation import (
FormValidator,
required_string,
email_field,
number_field,
)
# Create form validator
form = FormValidator()
form.add_field("username", required_string(min_length=3, max_length=20))
form.add_field("email", email_field())
form.add_field("age", number_field(min_value=18, max_value=120))
# Validate form data
data = {"username": "john_doe", "email": "john@example.com", "age": 25}
result = form.validate(data)
if result.is_valid:
print("Form is valid!")
else:
for error in result.errors:
print(f"{error.field_name}: {error.message}")
Integration with Input widgets::
from qtframework.widgets.inputs import Input
from qtframework.utils.validation import email_field
# Create input with validation
email_input = Input(
label="Email Address",
placeholder="Enter your email",
validators=email_field().validators,
)
# Validate on change
def on_email_change(text):
result = email_field().validate(text, "email")
if not result.is_valid:
email_input.set_error(result.get_error_messages()[0])
else:
email_input.clear_error()
email_input.textChanged.connect(on_email_change)
See Also:
:class:`Validator`: Base validator class for custom validators
:class:`FormValidator`: Multi-field form validation
:exc:`qtframework.utils.exceptions.ValidationError`: Validation error exception
:mod:`qtframework.widgets.inputs`: Input widgets with validation support
"""
from __future__ import annotations
import re
from abc import ABC, abstractmethod
from pathlib import Path
from typing import TYPE_CHECKING, Any
from qtframework.utils.exceptions import ValidationError
if TYPE_CHECKING:
from collections.abc import Callable
[docs]
class Validator(ABC):
"""Base class for validators."""
def __init__(self, message: str | None = None) -> None:
"""Initialize validator.
Args:
message: Custom error message
"""
self.message: str = message or "Validation failed"
[docs]
@abstractmethod
def validate(self, value: Any, field_name: str = "") -> bool:
"""Validate a value.
Args:
value: Value to validate
field_name: Name of the field being validated
Returns:
True if valid
Raises:
ValidationError: If validation fails
"""
def __call__(self, value: Any, field_name: str = "") -> bool:
"""Make validator callable."""
return self.validate(value, field_name)
[docs]
class RequiredValidator(Validator):
"""Validates that a value is not empty."""
def __init__(self, message: str | None = None) -> None:
"""Initialize required validator."""
super().__init__(message or "This field is required")
[docs]
def validate(self, value: Any, field_name: str = "") -> bool:
"""Validate that value is not empty."""
if value is None or value == "" or (hasattr(value, "__len__") and len(value) == 0):
raise ValidationError(
self.message, field_name=field_name, field_value=value, validation_rule="required"
)
return True
[docs]
class LengthValidator(Validator):
"""Validates string length."""
def __init__(
self,
min_length: int | None = None,
max_length: int | None = None,
message: str | None = None,
) -> None:
"""Initialize length validator.
Args:
min_length: Minimum length
max_length: Maximum length
message: Custom error message
"""
self.min_length = min_length
self.max_length = max_length
if not message:
if min_length and max_length:
message = f"Length must be between {min_length} and {max_length} characters"
elif min_length:
message = f"Length must be at least {min_length} characters"
elif max_length:
message = f"Length must not exceed {max_length} characters"
else:
message = "Invalid length"
super().__init__(message)
[docs]
def validate(self, value: Any, field_name: str = "") -> bool:
"""Validate string length."""
if not isinstance(value, str):
value = str(value) if value is not None else ""
length = len(value)
if self.min_length is not None and length < self.min_length:
raise ValidationError(
self.message,
field_name=field_name,
field_value=value,
validation_rule=f"min_length:{self.min_length}",
)
if self.max_length is not None and length > self.max_length:
raise ValidationError(
self.message,
field_name=field_name,
field_value=value,
validation_rule=f"max_length:{self.max_length}",
)
return True
[docs]
class RegexValidator(Validator):
"""Validates value against a regular expression."""
def __init__(self, pattern: str | re.Pattern[str], message: str | None = None) -> None:
"""Initialize regex validator.
Args:
pattern: Regular expression pattern
message: Custom error message
"""
self.pattern = re.compile(pattern) if isinstance(pattern, str) else pattern
super().__init__(message or f"Value does not match required pattern: {pattern}")
[docs]
def validate(self, value: Any, field_name: str = "") -> bool:
"""Validate value against regex pattern."""
if not isinstance(value, str):
value = str(value) if value is not None else ""
if not self.pattern.match(value):
raise ValidationError(
self.message,
field_name=field_name,
field_value=value,
validation_rule=f"regex:{self.pattern.pattern}",
)
return True
[docs]
class EmailValidator(RegexValidator):
"""Validates email addresses."""
EMAIL_PATTERN = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
def __init__(self, message: str | None = None) -> None:
"""Initialize email validator."""
super().__init__(self.EMAIL_PATTERN, message or "Please enter a valid email address")
[docs]
class NumberValidator(Validator):
"""Validates numeric values."""
def __init__(
self,
min_value: float | None = None,
max_value: float | None = None,
allow_float: bool = True,
message: str | None = None,
) -> None:
"""Initialize number validator.
Args:
min_value: Minimum value
max_value: Maximum value
allow_float: Whether to allow floating point numbers
message: Custom error message
"""
self.min_value = min_value
self.max_value = max_value
self.allow_float = allow_float
if not message:
if min_value is not None and max_value is not None:
message = f"Value must be between {min_value} and {max_value}"
elif min_value is not None:
message = f"Value must be at least {min_value}"
elif max_value is not None:
message = f"Value must not exceed {max_value}"
else:
message = "Invalid number"
super().__init__(message)
[docs]
def validate(self, value: Any, field_name: str = "") -> bool:
"""Validate numeric value."""
try:
if self.allow_float:
num_value = float(value)
else:
num_value = int(value)
if str(value).count(".") > 0:
raise ValueError("Float not allowed")
except (ValueError, TypeError):
raise ValidationError(
"Please enter a valid number",
field_name=field_name,
field_value=value,
validation_rule="number",
)
if self.min_value is not None and num_value < self.min_value:
raise ValidationError(
self.message,
field_name=field_name,
field_value=value,
validation_rule=f"min_value:{self.min_value}",
)
if self.max_value is not None and num_value > self.max_value:
raise ValidationError(
self.message,
field_name=field_name,
field_value=value,
validation_rule=f"max_value:{self.max_value}",
)
return True
[docs]
class PathValidator(Validator):
"""Validates file and directory paths."""
def __init__(
self,
must_exist: bool = False,
must_be_file: bool = False,
must_be_dir: bool = False,
message: str | None = None,
) -> None:
"""Initialize path validator.
Args:
must_exist: Whether path must exist
must_be_file: Whether path must be a file
must_be_dir: Whether path must be a directory
message: Custom error message
"""
self.must_exist = must_exist
self.must_be_file = must_be_file
self.must_be_dir = must_be_dir
super().__init__(message or "Invalid path")
[docs]
def validate(self, value: Any, field_name: str = "") -> bool:
"""Validate path."""
if not isinstance(value, str | Path):
raise ValidationError(
"Path must be a string or Path object",
field_name=field_name,
field_value=value,
validation_rule="path_type",
)
path = Path(value)
if self.must_exist and not path.exists():
raise ValidationError(
"Path does not exist",
field_name=field_name,
field_value=value,
validation_rule="path_exists",
)
if self.must_be_file and path.exists() and not path.is_file():
raise ValidationError(
"Path must be a file",
field_name=field_name,
field_value=value,
validation_rule="path_is_file",
)
if self.must_be_dir and path.exists() and not path.is_dir():
raise ValidationError(
"Path must be a directory",
field_name=field_name,
field_value=value,
validation_rule="path_is_dir",
)
return True
[docs]
class ChoiceValidator(Validator):
"""Validates that value is one of allowed choices."""
def __init__(self, choices: list[Any], message: str | None = None) -> None:
"""Initialize choice validator.
Args:
choices: List of allowed choices
message: Custom error message
"""
self.choices = choices
super().__init__(message or f"Value must be one of: {', '.join(map(str, choices))}")
[docs]
def validate(self, value: Any, field_name: str = "") -> bool:
"""Validate that value is in choices."""
if value not in self.choices:
raise ValidationError(
self.message,
field_name=field_name,
field_value=value,
validation_rule=f"choice:{self.choices}",
)
return True
[docs]
class CustomValidator(Validator):
"""Validates using a custom function."""
def __init__(self, func: Callable[[Any], bool], message: str | None = None) -> None:
"""Initialize custom validator.
Args:
func: Validation function that returns True if valid
message: Custom error message
"""
self.func = func
super().__init__(message or "Validation failed")
[docs]
def validate(self, value: Any, field_name: str = "") -> bool:
"""Validate using custom function."""
try:
if not self.func(value):
raise ValidationError(
self.message, field_name=field_name, field_value=value, validation_rule="custom"
)
except Exception as e:
if isinstance(e, ValidationError):
raise
raise ValidationError(
f"Validation error: {e}",
field_name=field_name,
field_value=value,
validation_rule="custom",
)
return True
[docs]
class ValidationResult:
"""Result of validation operation."""
def __init__(self, is_valid: bool = True, errors: list[ValidationError] | None = None) -> None:
"""Initialize validation result.
Args:
is_valid: Whether validation passed
errors: List of validation errors
"""
self.is_valid = is_valid
self.errors = errors or []
[docs]
def add_error(self, error: ValidationError) -> None:
"""Add validation error."""
self.errors.append(error)
self.is_valid = False
[docs]
def get_field_errors(self, field_name: str) -> list[ValidationError]:
"""Get errors for specific field."""
return [error for error in self.errors if error.field_name == field_name]
[docs]
def get_error_messages(self) -> list[str]:
"""Get all error messages."""
return [error.message for error in self.errors]
[docs]
def get_field_error_messages(self, field_name: str) -> list[str]:
"""Get error messages for specific field."""
return [error.message for error in self.get_field_errors(field_name)]
[docs]
class ValidatorChain:
"""Chain of validators for a field."""
def __init__(self, validators: list[Validator] | None = None) -> None:
"""Initialize validator chain.
Args:
validators: List of validators
"""
self.validators = validators or []
[docs]
def add_validator(self, validator: Validator) -> ValidatorChain:
"""Add validator to chain."""
self.validators.append(validator)
return self
[docs]
def validate(self, value: Any, field_name: str = "") -> ValidationResult:
"""Validate value using all validators in chain."""
result = ValidationResult()
for validator in self.validators:
try:
validator.validate(value, field_name)
except ValidationError as e:
result.add_error(e)
return result
# Predefined validator chains for common use cases
[docs]
def required_string(min_length: int = 1, max_length: int | None = None) -> ValidatorChain:
"""Create validator chain for required string."""
validators: list[Validator] = [RequiredValidator()]
if min_length > 1 or max_length:
validators.append(LengthValidator(min_length, max_length))
return ValidatorChain(validators)
[docs]
def optional_string(max_length: int | None = None) -> ValidatorChain:
"""Create validator chain for optional string."""
validators: list[Validator] = []
if max_length:
validators.append(LengthValidator(max_length=max_length))
return ValidatorChain(validators)
[docs]
def email_field() -> ValidatorChain:
"""Create validator chain for email field."""
return ValidatorChain([RequiredValidator(), EmailValidator()])
[docs]
def optional_email_field() -> ValidatorChain:
"""Create validator chain for optional email field."""
return ValidatorChain([EmailValidator()])
[docs]
def number_field(min_value: float | None = None, max_value: float | None = None) -> ValidatorChain:
"""Create validator chain for number field."""
return ValidatorChain([RequiredValidator(), NumberValidator(min_value, max_value)])
[docs]
def path_field(
must_exist: bool = False, must_be_file: bool = False, must_be_dir: bool = False
) -> ValidatorChain:
"""Create validator chain for path field."""
return ValidatorChain([
RequiredValidator(),
PathValidator(must_exist, must_be_file, must_be_dir),
])
[docs]
def choice_field(choices: list[Any]) -> ValidatorChain:
"""Create validator chain for choice field."""
return ValidatorChain([RequiredValidator(), ChoiceValidator(choices)])