Sindbad~EG File Manager
"""Validator functions for standard library types.
Import of this module is deferred since it contains imports of many standard library modules.
"""
from __future__ import annotations as _annotations
import math
import re
import typing
from ipaddress import IPv4Address, IPv4Interface, IPv4Network, IPv6Address, IPv6Interface, IPv6Network
from typing import Any, Callable
from pydantic_core import PydanticCustomError, core_schema
from pydantic_core._pydantic_core import PydanticKnownError
from pydantic_core.core_schema import ErrorType
def sequence_validator(
input_value: typing.Sequence[Any],
/,
validator: core_schema.ValidatorFunctionWrapHandler,
) -> typing.Sequence[Any]:
"""Validator for `Sequence` types, isinstance(v, Sequence) has already been called."""
value_type = type(input_value)
# We don't accept any plain string as a sequence
# Relevant issue: https://github.com/pydantic/pydantic/issues/5595
if issubclass(value_type, (str, bytes)):
raise PydanticCustomError(
'sequence_str',
"'{type_name}' instances are not allowed as a Sequence value",
{'type_name': value_type.__name__},
)
# TODO: refactor sequence validation to validate with either a list or a tuple
# schema, depending on the type of the value.
# Additionally, we should be able to remove one of either this validator or the
# SequenceValidator in _std_types_schema.py (preferably this one, while porting over some logic).
# Effectively, a refactor for sequence validation is needed.
if value_type is tuple:
input_value = list(input_value)
v_list = validator(input_value)
# the rest of the logic is just re-creating the original type from `v_list`
if value_type is list:
return v_list
elif issubclass(value_type, range):
# return the list as we probably can't re-create the range
return v_list
elif value_type is tuple:
return tuple(v_list)
else:
# best guess at how to re-create the original type, more custom construction logic might be required
return value_type(v_list) # type: ignore[call-arg]
def import_string(value: Any) -> Any:
if isinstance(value, str):
try:
return _import_string_logic(value)
except ImportError as e:
raise PydanticCustomError('import_error', 'Invalid python path: {error}', {'error': str(e)}) from e
else:
# otherwise we just return the value and let the next validator do the rest of the work
return value
def _import_string_logic(dotted_path: str) -> Any:
"""Inspired by uvicorn — dotted paths should include a colon before the final item if that item is not a module.
(This is necessary to distinguish between a submodule and an attribute when there is a conflict.).
If the dotted path does not include a colon and the final item is not a valid module, importing as an attribute
rather than a submodule will be attempted automatically.
So, for example, the following values of `dotted_path` result in the following returned values:
* 'collections': <module 'collections'>
* 'collections.abc': <module 'collections.abc'>
* 'collections.abc:Mapping': <class 'collections.abc.Mapping'>
* `collections.abc.Mapping`: <class 'collections.abc.Mapping'> (though this is a bit slower than the previous line)
An error will be raised under any of the following scenarios:
* `dotted_path` contains more than one colon (e.g., 'collections:abc:Mapping')
* the substring of `dotted_path` before the colon is not a valid module in the environment (e.g., '123:Mapping')
* the substring of `dotted_path` after the colon is not an attribute of the module (e.g., 'collections:abc123')
"""
from importlib import import_module
components = dotted_path.strip().split(':')
if len(components) > 2:
raise ImportError(f"Import strings should have at most one ':'; received {dotted_path!r}")
module_path = components[0]
if not module_path:
raise ImportError(f'Import strings should have a nonempty module name; received {dotted_path!r}')
try:
module = import_module(module_path)
except ModuleNotFoundError as e:
if '.' in module_path:
# Check if it would be valid if the final item was separated from its module with a `:`
maybe_module_path, maybe_attribute = dotted_path.strip().rsplit('.', 1)
try:
return _import_string_logic(f'{maybe_module_path}:{maybe_attribute}')
except ImportError:
pass
raise ImportError(f'No module named {module_path!r}') from e
raise e
if len(components) > 1:
attribute = components[1]
try:
return getattr(module, attribute)
except AttributeError as e:
raise ImportError(f'cannot import name {attribute!r} from {module_path!r}') from e
else:
return module
def pattern_either_validator(input_value: Any, /) -> typing.Pattern[Any]:
if isinstance(input_value, typing.Pattern):
return input_value
elif isinstance(input_value, (str, bytes)):
# todo strict mode
return compile_pattern(input_value) # type: ignore
else:
raise PydanticCustomError('pattern_type', 'Input should be a valid pattern')
def pattern_str_validator(input_value: Any, /) -> typing.Pattern[str]:
if isinstance(input_value, typing.Pattern):
if isinstance(input_value.pattern, str):
return input_value
else:
raise PydanticCustomError('pattern_str_type', 'Input should be a string pattern')
elif isinstance(input_value, str):
return compile_pattern(input_value)
elif isinstance(input_value, bytes):
raise PydanticCustomError('pattern_str_type', 'Input should be a string pattern')
else:
raise PydanticCustomError('pattern_type', 'Input should be a valid pattern')
def pattern_bytes_validator(input_value: Any, /) -> typing.Pattern[bytes]:
if isinstance(input_value, typing.Pattern):
if isinstance(input_value.pattern, bytes):
return input_value
else:
raise PydanticCustomError('pattern_bytes_type', 'Input should be a bytes pattern')
elif isinstance(input_value, bytes):
return compile_pattern(input_value)
elif isinstance(input_value, str):
raise PydanticCustomError('pattern_bytes_type', 'Input should be a bytes pattern')
else:
raise PydanticCustomError('pattern_type', 'Input should be a valid pattern')
PatternType = typing.TypeVar('PatternType', str, bytes)
def compile_pattern(pattern: PatternType) -> typing.Pattern[PatternType]:
try:
return re.compile(pattern)
except re.error:
raise PydanticCustomError('pattern_regex', 'Input should be a valid regular expression')
def ip_v4_address_validator(input_value: Any, /) -> IPv4Address:
if isinstance(input_value, IPv4Address):
return input_value
try:
return IPv4Address(input_value)
except ValueError:
raise PydanticCustomError('ip_v4_address', 'Input is not a valid IPv4 address')
def ip_v6_address_validator(input_value: Any, /) -> IPv6Address:
if isinstance(input_value, IPv6Address):
return input_value
try:
return IPv6Address(input_value)
except ValueError:
raise PydanticCustomError('ip_v6_address', 'Input is not a valid IPv6 address')
def ip_v4_network_validator(input_value: Any, /) -> IPv4Network:
"""Assume IPv4Network initialised with a default `strict` argument.
See more:
https://docs.python.org/library/ipaddress.html#ipaddress.IPv4Network
"""
if isinstance(input_value, IPv4Network):
return input_value
try:
return IPv4Network(input_value)
except ValueError:
raise PydanticCustomError('ip_v4_network', 'Input is not a valid IPv4 network')
def ip_v6_network_validator(input_value: Any, /) -> IPv6Network:
"""Assume IPv6Network initialised with a default `strict` argument.
See more:
https://docs.python.org/library/ipaddress.html#ipaddress.IPv6Network
"""
if isinstance(input_value, IPv6Network):
return input_value
try:
return IPv6Network(input_value)
except ValueError:
raise PydanticCustomError('ip_v6_network', 'Input is not a valid IPv6 network')
def ip_v4_interface_validator(input_value: Any, /) -> IPv4Interface:
if isinstance(input_value, IPv4Interface):
return input_value
try:
return IPv4Interface(input_value)
except ValueError:
raise PydanticCustomError('ip_v4_interface', 'Input is not a valid IPv4 interface')
def ip_v6_interface_validator(input_value: Any, /) -> IPv6Interface:
if isinstance(input_value, IPv6Interface):
return input_value
try:
return IPv6Interface(input_value)
except ValueError:
raise PydanticCustomError('ip_v6_interface', 'Input is not a valid IPv6 interface')
def forbid_inf_nan_check(x: Any) -> Any:
if not math.isfinite(x):
raise PydanticKnownError('finite_number')
return x
_InputType = typing.TypeVar('_InputType')
def create_constraint_validator(
constraint_id: str,
predicate: Callable[[_InputType, Any], bool],
error_type: ErrorType,
context_gen: Callable[[Any, Any], dict[str, Any]] | None = None,
) -> Callable[[_InputType, Any], _InputType]:
"""Create a validator function for a given constraint.
Args:
constraint_id: The constraint identifier, used to identify the constraint in error messages, ex 'gt'.
predicate: The predicate function to apply to the input value, ex `lambda x, gt: x > gt`.
error_type: The error type to raise if the predicate fails.
context_gen: A function to generate the error context from the constraint value and the input value.
"""
def validator(x: _InputType, constraint_value: Any) -> _InputType:
try:
if not predicate(x, constraint_value):
raise PydanticKnownError(
error_type, context_gen(constraint_value, x) if context_gen else {constraint_id: constraint_value}
)
except TypeError:
raise TypeError(f"Unable to apply constraint '{constraint_id}' to supplied value {x}")
return x
return validator
_CONSTRAINT_TO_VALIDATOR_LOOKUP: dict[str, Callable] = {
'gt': create_constraint_validator('gt', lambda x, gt: x > gt, 'greater_than'),
'ge': create_constraint_validator('ge', lambda x, ge: x >= ge, 'greater_than_equal'),
'lt': create_constraint_validator('lt', lambda x, lt: x < lt, 'less_than'),
'le': create_constraint_validator('le', lambda x, le: x <= le, 'less_than_equal'),
'multiple_of': create_constraint_validator(
'multiple_of', lambda x, multiple_of: x % multiple_of == 0, 'multiple_of'
),
'min_length': create_constraint_validator(
'min_length',
lambda x, min_length: len(x) >= min_length,
'too_short',
lambda c_val, x: {'field_type': 'Value', 'min_length': c_val, 'actual_length': len(x)},
),
'max_length': create_constraint_validator(
'max_length',
lambda x, max_length: len(x) <= max_length,
'too_long',
lambda c_val, x: {'field_type': 'Value', 'max_length': c_val, 'actual_length': len(x)},
),
}
def get_constraint_validator(constraint: str) -> Callable:
"""Fetch the validator function for the given constraint."""
try:
return _CONSTRAINT_TO_VALIDATOR_LOOKUP[constraint]
except KeyError:
raise TypeError(f'Unknown constraint {constraint}')
IP_VALIDATOR_LOOKUP: dict[type, Callable] = {
IPv4Address: ip_v4_address_validator,
IPv6Address: ip_v6_address_validator,
IPv4Network: ip_v4_network_validator,
IPv6Network: ip_v6_network_validator,
IPv4Interface: ip_v4_interface_validator,
IPv6Interface: ip_v6_interface_validator,
}
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists