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.serverimportPropertyclassJPEG(Property):"""JPEG image data"""def__init__(self,default=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)assert(isinstance(compression_ratio,int)andcompression_ratio>=0andcompression_ratio<=9),"compression_ratio must be an integer between 0 and 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:
importtyping,numpy,imageiofromhololinked.serverimportPropertyfromhololinked.param.parameterizedimportinstance_descriptorclassJPEG(Property):"""JPEG image data"""@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)returnsuper().__set__(obj,imageio.imwrite('<bytes>',value,format='JPEG',compress_level=self.compression_ratio))raiseValueError(f"invalid type for JPEG image data - {type(value)}")
Basically, check the types, manipulate your data if necessary and 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, writing the value to the database etc. To avoid double checking of certain options like readonly and constant, its better to carry out the validation and coercion within the method validate_and_adapt() instead of __set__:
classJPEG(Property):"""JPEG image data"""self.flip_vertical=flip_verticaldefvalidate_and_adapt(self,value)->typing.Any:ifvalueisNoneandnotself.allow_None:# no need to check readonly & constant raiseValueError("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(), therefore the new value or validated value can be returned from this method.
To use the JPEG property in a Thing class, follow the normal procedure of property instantiation:
fromhololinked.serverimportThingclassCamera(Thing):_image=JPEG(doc="Image data in JPEG format",compression_ratio=2,transpose=False,flip_horizontal=True,remote=False)# type: bytesimage=JPEG(readonly=True,doc="Image data in JPEG format",fget=lambdaself:self._image)# type: bytesdefcapture(self):whileTrue:# 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 fetch the processed image.
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):horizontally_flipped_image=JPEG(doc="Image data in JPEG format",flip_horizontal=True)# type: bytesvertically_flipped_image=JPEG(doc="Image data in JPEG format",flip_vertical=True)# type: bytestransposed_image=JPEG(doc="Image data in JPEG format",flip_horizontal=False,flip_vertical=False,transpose=True)# type: bytes
Note
This is a contrived example and may not lead to optimized Property APIs for the client. Please adapt them suitably or rethink their implementation.
One may also use slots to store the attributes of the Property. Most properties predefined in this package use slots: