Package: exert
Exert>>Declaratively apply converter functions to class attributes.<<InstallationInstall via pip:pip install exertUsageUse this to declaratively apply arbitrary converter functions to the attributes of a
class. For example:from__future__importannotationsfromtypingimportAnnotatedfromexertimportexert,Mark@exertclassFoo:a:Annotated[int,Mark(lambdax:x**2)]b:Annotated[float,Mark(lambdax:x/2)]def__init__(self,a:int,b:float)->None:self.a=aself.b=bfoo=Foo(2,42.0)print(foo.a)# prints 4print(foo.b)# prints 21.0Here, the lambda function tagged withMarkis the converter.Use with dataclassesDataclasses can also be used to avoid writing the initializer by hand. For example:...fromdataclassesimportdataclass@exert@datclassesclassFoo:a:Annotated[int,Mark(lambdax:x**2)]b:Annotated[float,Mark(lambdax:x/2)]foo=Foo(2,42.0)print(foo.a)# prints 4print(foo.b)# prints 21.0Apply multiple converters sequentiallyMultiple converters are allowed. For example:...@exert@dataclassclassFoo:a:Annotated[int,Mark(lambdax:x**2,lambdax:x**3)]b:Annotated[float,Mark(lambdax:x/2,lambdax:x/3)]foo=Foo(2,42.0)print(foo.a)# prints 64  [2**2=4, 4**3=64]print(foo.b)# prints 7.0 [42.0/2=21.0, 21.0/3=7.0]Here, the converters are applied sequentially. The result of the preceding converter is
fed into the succeeding converter as input. You've to make sure that the number of the
returned values of the preceding converter matches that of the succeeding converter.Exclude annotated fieldsIf you don't wrap converters withMark, the corresponding field won't be transformed:...@exert@dataclassclassFoo:a:Annotated[int,Mark(lambdax:x**2,lambdax:x**3)]b:Annotated[float,lambdax:x/2,lambdax:x/3]foo=Foo(2,42.0)print(foo.a)# prints 64  [2**2=4, 4**3=64]print(foo.b)# prints 42.0 [This field was ignored]Since the converters in fieldbweren't tagged withMark, no conversion happened.Apply common converters without repetitionCommon converters can be applied to multiple fields without repetition:...@exert(converters=(lambdax:x**2,))@dataclassclassFoo:a:Annotated[int,None]b:Annotated[float,None]foo=Foo(2,42.0)print(foo.a)# prints 4      [2**2=4]print(foo.b)# prints 1764.0 [42.0**2=1764.0]Apply common and marked converters togetherYou can apply a sequence of common converters and marked converters together. By default,
the common converters are applied first and then the tagged converters are applied
sequentially:...@exert(converters=(lambdax:x**2,lambdax:x**3))@dataclassclassFoo:a:Annotated[int,Mark(lambdax:x/100)]b:Annotated[float,None]foo=Foo(2,42.0)print(foo.a)# prints 0.64         [2**2=4, 4**3=64, 64/100=0.64]print(foo.b)# prints 5489031744.0 [42.0**2=1764, 1764**3=5489031744.0]You can also, choose to apply the common converters after the tagged ones. For this,
you'll need to set theapply_lastparameter toTrue:...@exert(converters=(lambdax:x**2,lambdax:x**3),apply_last=True,)@dataclassclassFoo:a:Annotated[int,Mark(lambdax:x/100)]b:Annotated[float,None]foo=Foo(2,42.0)print(foo.a)# prints 6.401e-11 [2/100=0.02, 0.02**2=0.004, 0.0004**3=6.401e-11]print(foo.b)# prints 5489031744.0 [42.0**2=1764, 1764**3=5489031744.0]Simple data validationThe snippet below ensures that the attributes of the classFooconforms to the constraints imposed by the converter functions.from__future__importannotationsimportjsonfromdataclassesimportdataclassfromfunctoolsimportpartialfromtypingimportAnnotated,SizedfromexertimportMark,exertdefassert_len(x:Sized,length:int)->Sized:"""Assert that the incoming attribute has the expected length."""assertlen(x)==lengthreturnxdefassert_json(x:dict)->dict:"""Assert that the incoming attribute is JSON serializable."""try:json.dumps(x)exceptTypeError:raiseAssertionError(f"{x}is not JSON serializable")returnx@exert@dataclassclassFoo:a:Annotated[tuple[int,...],Mark(partial(assert_len,length=3))]b:Annotated[dict,Mark(assert_json)]foo=Foo(a=(1,2,3),b={"a":1,"b":2})print(foo)This will raiseAssertionErrorif the value of the attributes violate the constraints.✨ 🍰 ✨