Package: powerdataclass
⚡ Power Dataclass ⚡Installationpip install powerdataclassUsagePython 3.7 have introduced a spiritual successor ofNamedTuple: thedataclass.
While being nice, thedataclasstype hinting is only, well,hinting.This library gives you an ability to create dataclasses with field values automatically cast to
the types defined in thedataclass's type hints:Typecastingfrompowerdataclassimport*classCoordinates(PowerDataclass):x:inty:intc1=Coordinates(1,2)c2=Coordinates('1','2')c3=Coordinates(**{'x':1.1,'y':2.2})# >>> c1Coordinates(x=1,y=2)# >>> c1 == c2 == c3TrueThis also works with every generic type that has a Python primitive type as its origin. This applies to subscriptable
types of any level of nestedness as well:classVector(PowerDataclass):items:List[int]v1=Vector(['1','2','3'])v2=Vector({1.1,2.2,3.3})v3=Vector(range(1,4))# >>> v1Vector(items=[1,2,3])# >>> v1 == v2 == v3TrueThe typecasting also respects other dataclasses (and Power Dataclasses) declared in type hints.
If you pass a mapping or an iterable in place of actual dataclass instance, Power Dataclass will attempt to unpack it to
a corresponding dataclass:classVector(PowerDataclass):items:List[int]classTensor(PowerDataclass):vectors:List[Vector]t1=Tensor(**{'vectors':[{'items':[1,2,3]},{'items':[4,5,6]},([7,8,9],),]})# >>> t1Tensor(vectors=[Vector(items=[1,2,3]),Vector(items=[4,5,6]),Vector(items=[7,8,9])])If a value type is defined as a dataclass and that dataclass can be instantiated with a sole argument,
it will be cast as wellclassTimestampedIntValue(PowerDataclass):value:inttimestamp:int=time.time()classSensorReadings(PowerDataclass):moon_phase_angle:TimestampedIntValuemars_surface_temperature:TimestampedIntValuereadings=SensorReadings(122,-70)# >>> readingsSensorReadings(moon_phase_angle=TimestampedIntValue(value=122,timestamp=1570898094),mars_surface_temperature=TimestampedIntValue(value=-70,timestamp=1570898094))Custom typecastingYou can modify the behaviour of type casting by registering two types of handlers on your fancy PowerDataclass:type handlers: a unary method marked as atype handlerwill be applied to any value that has a matching type
declared in your dataclass typehints.field handlers: a unary method marked as afield handlerwill be applied to a value of a specific PDC field.Those functions mustalwaysreturn a value.You can do this by marking your methods with special decorators:classCoolBool(PowerDataclass):string_bool:boolnegated_bool:bool@type_handler(bool)defhandle_bools(self,v):iftype(v)isstr:returnv.lower()in['y','yes','1','True']else:returnbool(v)@field_handler('negated_bool')defhandle_negated_bools(self,v):returnnotself.handle_bools(v)# >>> CoolBool('yes', 'no')CoolBool(string_bool=True,negated_bool=True)Field handlers take precedence over the type handlers.
Field handlers and type handlers are scoped to a particular Power Dataclass. Inheritance is respected.Field MetadataThe behaviour of fields can be modified by providing corresponding flags in a field'smetadatadictionary,
provided by base Pythondataclasses.NullabilityFields are considered non-nullable by default.
This means that if, during instantiation, the value of a field will be equal toNone, aValueErrorexception will
occur.
Type casting will be performed on non-null values, except for non-typecast fields (see below)If a field has a default value, and it isNone, it will be considered nullable.
Also, if you want to acceptNonebut you either don't want to provide defaults at all, provide a non-null default or
provide a default factory, you can mark your field as nullable by either setting the flag or using a pre-made partial:classNihilus(PowerDataclass):x:int=field(metadata={FieldMeta.NULLABLE:True})y:int=Nonez:list=nullable_field(default_factory=list)# >>> Nihilus()# ! TypeError: __init__() missing 1 required positional argument: 'x'# >>> Nihilus(1)Nihilus(x=1,y=None,z=[])# >>> Nihilus('1', '1', (1,))Nihilus(x=1,y=1,z=[1])# >>> Nihilus('1', None, None)Nihilus(x=1,y=1,z=None)Skipping typecasting (and null checking)If you want to disable type checking for a specific field you can mark your field as nullable by either setting the
corresponding flag in the fields'metadatadictionary or using a pre-made partial:classNoncasted(PowerDataclass):x:int=field(metadata={FieldMeta.SKIP_TYPECASTING:True})y:int=noncasted_field()# >>> Noncasted('1', 2.2)Noncasted(x='1',y=2.2)Dependent and calculated fieldsIf some of your field processing requires other fields typecast before you can declare this field dependencies by name
by setting the corresponding value in the fields'metadata:classDependent(PowerDataclass):a:intb:int=field(metadata={FieldMeta.DEPENDS_ON_FIELDS:['a']})c:int=field(metadata={FieldMeta.DEPENDS_ON_FIELDS:['d','b']})d:int=field(metadata={FieldMeta.DEPENDS_ON_FIELDS:['a']})Fields will be topologically sorted by their dependencies and type casting will be done in this order. For this example,
the order will be:abdcYou can use a combination of field handlers and dependent fields to declare calculated fields:classCubeSquarer(PowerDataclass):n:intn_square:int=field(default=None,metadata={FieldMeta.DEPENDS_ON_FIELDS:['n']})n_cube:int=calculated_field(depends_on=['n'])@field_handler('n_square')defhandle_n_square(self,v):returnself.n**2@field_handler('n_cube')defhandle_n_cube(self,v):returnself.n**3# >>> CubeSquarer(4)CubeSquarer(n=4,n_square=16,n_cube=256)It is an error to declare a field ascalculatablewithout registering a correspondingfield_handlerModification of Power Dataclass behaviourYou can modify the behaviour of Power Dataclass by editing theMetanested class' attributes.
All Power Dataclasses have a default value for thisMetanested class equal
topowerdataclass.PowerDataclassDefaultMetaThisMetasubclass will emulate the behaviour of class variable inheritance, making every attribute ofMetadefault
topowerdataclass.PowerDataclassDefaultMetaCurrently, the following values are now supported:NameDefault valueDescriptiondataclass_initTruepassed to thedataclasses.dataclassconstructor.See docsdataclass_reprTruepassed to thedataclasses.dataclassconstructor.dataclass_eqTruepassed to thedataclasses.dataclassconstructor.dataclass_orderFalsepassed to thedataclasses.dataclassconstructor.dataclass_unsafe_hashFalsepassed to thedataclasses.dataclassconstructor.dataclass_frozenFalsepassed to thedataclasses.dataclassconstructor.singletonFalseIfTrueenables theSingleton Mode.json_encoderNoneIf set, this class will be used as aclsparam tojson.dumpsinPowerDataclass().to_json()See docs.json_decoderNoneIf set, this class will be used as aclsparam tojson.loadsinPowerDataclass.from_json()See docs.as_dict_ignored_when_nestedFalseIf set to True, this PDC won't be converted when this PDC is nested and wrapping PDC's.as_dict()is called. Can be further ignored ifas_dict(force=True)was called.Example of setting theMetaof aPowerDataclass:classPowerDataclassWithNewBehaviour(PowerDataclass):classMeta:dataclass_frozen=Truesingleton=TrueSingleton ModeIf you set theMeta.singletonvalue toTrue, your PowerDataclass will turn into
aSingleton.This means that this PowerDataclass can be instantiated only once, and all further attempts to instantiate this PDC will
return that instance instead:classPDCSingleton(PowerDataclass):a:intclassMeta:singleton=Truesingleton1=PDCSingleton(1)singleton2=PDCSingleton(2)# >>> id(singleton1) == id(singleton2)TrueYou can test whether a Singleton has been instantiated by calling the class method.get_instance()on your Singleton
Mode class.
If there is an instance, it will be returned. Otherwise,Nonewill lbe returned.Other featuresAutomatic recursive conversion to dict with the.as_dict()method.Automatic recursive conversion to and from JSON strings with the.as_json()and.from_json()methods.PowerDataclass mergingThePowerDataclass.merge(other)allows you to merge two PowerDataclasses, rewriting the fields' values of the first
PDC with the corresponding values of the second PDCs, while retaining the memory address of the first PDC.classPDC(PowerDataclass):x:inty:intz:inta=PDC(1,2,3)b=PDC(3,4,5)a.merge(b)# >>> id(a) != id(b)True# >>> a.as_dict() == b.as_dict()TruePowerDataclass diffA simple dictionary diff can be calculated between two instances of a same PowerDataclass by using the.diff()methodclassDiffPDC(PowerDataclass):x:inty:intz:inta=DiffPDC(1,2,3)b=DiffPDC(3,4,5)# >>> a.diff(b){'x':(1,3),'y':(2,4),'z':(3,5)}Note that an attempt to comparePowerDataclasses of different type will result aDiffImpossibleexceptionPowerConfigThepowerdataclass.powerconfigpackage contains two pre-made classes suitable for simple configuration management in
your services.
Those classes are: thePowerConfigand it's singleton mode subclass, theGlobalPowerConfigBoth of those share two extensions over regularPowerDataclass:APowerConfigcan be instantiated from the os environment.PowerDataclass.Meta.envvar_prefixwill be prepended
to capitalized names ofPowerConfig's fields' names.classConfig(PowerConfig):a:intclassMeta:envvar_prefix="CNF"# >>> Config.from_environ()Config(a=5)This class method will read the OS environment variableCNF_A. In this example. it ts assumed that this variable is
present and is equal to5.there is a predefinedtype_handlerfor thebooltype, which casts string values in(y, yes, 1, True)toTrue.Made with ⚡ by Arish Pyne (https://github.com/arishpyne/powerdataclass)