Since I have to validate class attributes (comparables) constantly, it seems sensible to put this into a more or less universal descriptor that should validate the type of argument being set and the range of values if that is acceptable.
from typing import TypeVar
T = TypeVar("T", bound=type)
Instance = TypeVar("Instance", bound=T) # ???
class TypeAndValueValidator:
def __init__(
self,
type_: T = None,
start_value=None,
end_value=None,
include_start: bool = True,
include_end: bool = True,
):
self.type = type_
self.start_value = start_value
self.end_value = end_value
self.include_start = include_start
self.include_end = include_end
def __set_name__(self, owner, name):
…
def __get__(self, obj, obj_type=None):
...
def __set__(self, obj, value):
…
How to express it:
-
The
type_
attribute is a class. -
The
start_value
and end_value are instances of the class specified in thetype_
attribute.
Accordingly – then I can specify type constraints for the parameters of the methods set_name
, get
, set
, etc.
I tried to figure this out using the typing documentation, but it remained unclear to me.
Kostiantyn Zivenko is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
3
To meet your requirements, you can simplify the use of type constraints in the TypeAndValueValidator
class. You don’t need the second TypeVar
. Instead, you can use Type[T]
to denote the type of a class (i.e., a class object) that produces instances of T
. Here’s an updated implementation:
from typing import Type, TypeVar, Optional
T = TypeVar("T")
class TypeAndValueValidator:
def __init__(
self,
type_: Optional[Type[T]] = None,
start_value: Optional[T] = None,
end_value: Optional[T] = None,
include_start: bool = True,
include_end: bool = True,
):
self.type = type_
self.start_value = start_value
self.end_value = end_value
self.include_start = include_start
self.include_end = include_end
def __set_name__(self, owner, name: str):
self.name = name
def __get__(self, obj, obj_type=None):
return getattr(obj, f"_{self.name}", None)
def __set__(self, obj, value: T):
if self.type is not None and not isinstance(value, self.type):
raise TypeError(f"{self.name} must be of type {self.type.__name__}")
if self.start_value is not None:
if (self.include_start and value < self.start_value) or (
not self.include_start and value <= self.start_value
):
raise ValueError(f"{self.name} must be >= {self.start_value}")
if self.end_value is not None:
if (self.include_end and value > self.end_value) or (
not self.include_end and value >= self.end_value
):
raise ValueError(f"{self.name} must be <= {self.end_value}")
setattr(obj, f"_{self.name}", value)
Explanation:
Using Type[T]:
- The
type_
argument is declared asOptional[Type[T]]
. This means it
accepts either a class (e.g.,int
,str
) orNone
. start_value
andend_value
are of typeOptional[T]
, meaning they must
be instances of the specifiedtype_
class (if provided) orNone
.
Validation in __set__
:
- It checks whether the
value
being set is an instance oftype_
. - It validates the range constraints
(start_value
andend_value
).
Dynamic Attribute Storage:
The __set_name__
method assigns a private name (_{name}
) to store the actual value dynamically.
Usage Example:
class MyClass:
value = TypeAndValueValidator(int, start_value=10, end_value=20)
obj = MyClass()
obj.value = 15 # Works
obj.value = 25 # Raises ValueError: value must be <= 20