from webspirit.config.logger import DEBUG, INFO, debug, info, error, warning, critical
from functools import partial, update_wrapper, wraps
from inspect import BoundArguments, signature
from typing import Any, Callable, Self
from .contexterror import ecm, re
from types import UnionType
from pathlib import Path
__all__: list[str] = [
'CheckType',
# 'ValidatePathOrUrl',
]
[docs]
class CheckType:
SELF: str = 'self'
RETURN: str = 'return'
def __init__(self, *parameters: tuple, convert: bool = True, return_: bool = False):
if len(parameters) == 1 and not isinstance(parameters[0], str):
self.without_parentheses: bool = True
self.function: Callable[..., Any] = parameters[0]
else:
self.without_parentheses: bool = False
self.name_parameters = parameters
self._return = return_
self._convert = convert
def __call_with_parenthesis__(self) -> Callable[..., Any]:
@wraps(self.function)
def wrapper(cls: Self | Any, *args: tuple, **kwargs: dict) -> Any:
self.signature: BoundArguments = signature(self.function).bind(cls, *args, **kwargs) # type: ignore
self.signature.apply_defaults()
self.arguments: dict[str, object] = dict(self.signature.arguments) # type: ignore
self.arguments_no_self: dict[str, object] = self.arguments.copy() # type: ignore
self.arguments_no_self.pop(CheckType.SELF, None)
empty_call: bool = not bool(self.name_parameters)
for parameter in self.name_parameters:
if parameter not in self.arguments_no_self.keys():
re(f"'{parameter}' parameter isn't defined in the {self.function.__name__}({', '.join(self.arguments.keys())})")
if any(
parameter not in self.annotations_no_return for parameter in self.arguments_no_self
) and empty_call:
re(f"You must annotate parameters of {self.function.__name__}({': <type>, '.join(self.arguments.keys())}: <type>)")
if any(
parameter not in self.annotations_no_return for parameter in self.name_parameters
) and not empty_call:
re(f"You must annotate '{', '.join(self.name_parameters)}' {'parameter(s)' if len(self.name_parameters) > 1 else 'parameter'} of {self.function.__name__}({': <type>, '.join(self.name_parameters)}: <type>)")
if empty_call:
for parameter in self.annotations_no_return:
self.validate_and_convert_type(parameter)
else:
for parameter in self.name_parameters:
self.validate_and_convert_type(parameter)
_return: Any = self.function(*self.signature.args, **self.signature.kwargs)
return self.convert(CheckType.RETURN, _return, self.annotations[CheckType.RETURN]) if self._return else _return
return wrapper
def __call_without_parenthesis__(self, *args: tuple, **kwargs: dict) -> Any:
self.signature: BoundArguments = signature(self.function).bind(*args, **kwargs)
self.signature.apply_defaults()
self.arguments: dict[str, object] = dict(self.signature.arguments)
self.arguments_no_self: dict[str, object] = self.arguments.copy()
self.arguments_no_self.pop(CheckType.SELF, None)
if any(
parameter not in self.annotations_no_return for parameter in self.arguments_no_self
):
re(f"You must annotate parameters of {self.function.__name__}({': <type>, '.join(self.arguments.keys())}: <type>)")
for parameter in self.annotations_no_return:
self.validate_and_convert_type(parameter)
_return: Any = self.function(*self.signature.args, **self.signature.kwargs)
return self.convert(CheckType.RETURN, _return, self.annotations[CheckType.RETURN]) if self._return else _return
def __call__(self, *args: tuple, **kwargs: dict) -> Callable[..., Any] | Any:
if not self.without_parentheses:
self.function: Callable[..., Any] = args[0]
update_wrapper(self, self.function)
self.annotations: dict[str, Any] = self.function.__annotations__
self.annotations_no_return: dict[str, Any] = self.annotations.copy()
self.annotations_no_return.pop(CheckType.RETURN, None)
self.annotations_clean: dict[str, Any] = self.annotations_no_return.copy()
self.annotations_clean.pop(CheckType.SELF, None)
if not self.annotations_clean:
warning(f"{self.function.__name__} function hasn't parameters")
if self._return and self.annotations.get(CheckType.RETURN) is None:
re(f"You must annotate the return of {self.function.__name__}(...) -> <type>")
if self.without_parentheses:
return self.__call_without_parenthesis__(*args, **kwargs)
else:
return self.__call_with_parenthesis__()
def __get__(self, object_, type_=None):
if object_ is None:
if isinstance(self.function, classmethod):
return partial(self.__call__, type_)
else:
return self.__call__
return partial(self.__call__, object_)
[docs]
def validate_and_convert_type(self, parameter: str):
given: object = self.arguments[parameter]
asked: type = self.annotations[parameter]
if type(given) != asked:
if self._convert:
self.signature.arguments[parameter] = self.convert(parameter, given, asked)
else:
re(f"The parameter {parameter} of {self.function.__name__} with a '{given}' value must be of type {asked} but you have given '{given}' with a type {type(given)}")
[docs]
def convert(self, parameter: str, value: object, annotation: type | UnionType | str) -> object | None:
flag: bool = annotation is None or 'None' in str(annotation) # None is in annotation of the argument
is_none: bool = value is None # The provided argument is None
if is_none and flag:
return None
if isinstance(annotation, UnionType):
annotation: str = str(annotation)
if isinstance(annotation, str) and ' | ' in annotation:
annotations: list[type] = []
# An error has occurred, with the converting of '{annotation}' annotations to a list of available types (Use this syntax : 'type1 | type2 | ... | None' in incremental order of preference)
for i, _type in enumerate(annotation.split(' | ')):
with ecm(f"Can't convert string annotation '{_type}' to a real type (n°{i} of {annotation.split(' | ')})", level=DEBUG):
annotations.append(eval(_type.split('.')[-1]))
else:
annotations: list[type] = [annotation]
with ecm():
annotations.remove(None)
for i, _type in enumerate(annotations):
with ecm(f"The parameter {parameter} of {self.function.__name__} with a '{value}' value can't be converted to {_type} (n°{i} of {annotations})", level=INFO):
if type(value) is _type:
debug(f"Skip converting for the parameter {parameter} of {self.function.__name__} with a '{value}' value and a type {type(value)} because is already of type {_type}")
converted = value
else:
converted = _type(value)
debug(f"Change the parameter {parameter} of {self.function.__name__} with a '{value}' value and a type {type(value)} to type {_type}")
return converted
if flag:
return None
re(f"The parameter {parameter} of {self.function.__name__} with a '{value}' value can't be converted to one of {annotation}", ValueError)
class ValidatePathOrUrl(CheckType):
def __init__(self, *parameters: tuple, convert: bool = True, exist: bool = False):
self.exist = exist
super().__init__(*parameters, convert=convert)
"""
def convert(self, parameter: str, value: str | Path | PathOrURL | None, annotation: type[PathOrURL]) -> PathOrURL:
if isinstance(value, annotation) or value is None:
return value
returned: PathOrURL | None = None
if HyperLink.is_url(str(value)) and issubclass(HyperLink, annotation):
returned = HyperLink(value)
if StrPath.is_path(value, dir=False, suffix=['csv', 'txt']) and issubclass(StrPath, annotation):
returned = StrPath(value)
if self.exist and issubclass(StrPath, annotation):
with Path(value).open('w', encoding='utf-8'): pass
returned = StrPath(value)
info(f"Create {Path(value)}, because doesn't exist")
if returned is not None:
debug(f"Change '{value}' of type {type(value)} to type {type(returned)}")
return returned
_re(f"'{parameter}' object with '{value}' must be a valid url or a path to a csv or a txt file", ValueError)
"""