Properties can also be extended to define custom types, validation and coercion based on specific requirements. As a contrived example, one may define a JPEG image attribute which may accept a numpy array as input, have a compression level setting, transpose and flip the image if necessary.
To create the property, inherit from the Property object and define the __init__:
fromhololinked.coreimportProperty,ThingclassJPEG(Property):"""JPEG image data"""def__init__(self,default:Optional[numpy.ndarray]=None,compression_ratio:int=1,transpose:bool=False,flip_horizontal:bool=False,flip_vertical:bool=False,**kwargs,)->None:super().__init__(default=default,allow_None=True,**kwargs)if(notisinstance(compression_ratio,int)ornot0<=compression_ratio<=9):raiseValueError("compression_ratio must be an integer from 0 to 9")self.compression_ratio=compression_ratioself.transpose=transposeself.flip_horizontal=flip_horizontalself.flip_vertical=flip_vertical
It is possible to use the __set__() to carry out type validation & coercion. This method is automatically invoked by python:
importtyping,numpy,imageiofromhololinked.coreimportProperty,Thingfromhololinked.param.parameterizedimportinstance_descriptorclassJPEG(Property):"""JPEG image data"""raiseValueError(f"invalid type for JPEG image data - {type(value)}")@instance_descriptordef__set__(self,obj,value)->None:ifself.readonly:raiseAttributeError("Cannot set read-only image attribute")ifvalueisNoneandnotself.allow_None:raiseValueError("None is not allowed")ifisinstance(value,bytes):raiseValueError("Supply numpy.ndarray instead of pre-encoded JPEG image")ifisinstance(value,numpy.ndarray):ifself.flip_horizontal:value=numpy.fliplr(value)ifself.flip_vertical:value=numpy.flipud(value)ifself.transpose:value=numpy.transpose(value)binary=(imageio.imwrite("<bytes>",value,format="JPEG",compress_level=self.compression_ratio,),)returnsuper().__set__(obj,binary)raiseValueError(f"invalid type for JPEG image data - {type(value)}")
Essentially:
check metadata options like readonly, constant etc.
check the type of the input value
manipulate your data if necessary
pass it to the parent.
It is necessary to use the instance_descriptor decorator as shown above to allow class_member option to function correctly. If the Property will not be a class_member, this decorator can be skipped.
Further, the parent class Property takes care of allocating an instance variable, checking constant, readonly, pushing change events etc. Therefore, to avoid redundancy, its recommended to implement a validate_and_adapt() method instead of __set__:
classJPEG(Property):"""JPEG image data"""defvalidate_and_adapt(self,value)->bytes:ifvalueisNoneandnotself.allow_None:# no need to check readonly & constantraiseValueError("image attribute cannot take None")ifisinstance(value,bytes):raiseValueError("Supply numpy.ndarray instead of pre-encoded JPEG image")ifisinstance(value,numpy.ndarray):ifself.flip_horizontal:value=numpy.fliplr(value)ifself.flip_vertical:value=numpy.flipud(value)ifself.transpose:value=numpy.transpose(value)returnimageio.imwrite("<bytes>",value,format="JPEG",compress_level=self.compression_ratio,)raiseValueError(f"invalid type for JPEG image data - {type(value)}")
The __set__() method automatically invokes validate_and_adapt(), and a return value is expected.
To use the JPEG property in a Thing class, follow the normal procedure of property instantiation:
classCamera(Thing):"""Example object with custom defined JPEG property"""_image=JPEG(doc="Image data in JPEG format, not exposed to client, used internally",compression_ratio=2,transpose=False,flip_horizontal=True,remote=False,)# type: bytesimage=JPEG(readonly=True,# dont allow clients to manipulate camera's imagedoc="latest captured image data in JPEG format",fget=lambdaself:self._image,)# type: bytesdefcapture(self):whileTrue:image=self._capture_image()# write image capture logic hereself._image=image# captured image
In this particular example, since we dont want the JPEG to be set externally by a client, we create a local Property which carries out the image manipulation and an externally visible readonly Property that can supply the processed image to the client.
The difference between using a custom setter/fset method and overloading the Property is that, one can accept certain options specific to the Property in the __init__ of the
Property:
classCamera(Thing):"""Example object with custom defined JPEG property"""horizontally_flipped_image=JPEG(doc="Image data in JPEG format, flipped horizontally",flip_horizontal=True,)# type: bytesvertically_flipped_image=JPEG(doc="Image data in JPEG format, flipped vertically",flip_vertical=True)# type: bytestransposed_image=JPEG(doc="Image data in JPEG format, transposed",flip_horizontal=False,flip_vertical=False,transpose=True,)# type: bytes
One may also use slots to store the attributes of the Property. Most properties predefined in this package use slots: