Package: kisa
Kisa - Python Object Oriented SystemKisa is an advanced, comprehensive Object Oriented System written for Python.Kisa takes on itself many of the repetitive, unnecessary code sections and provides them automatically, such as:Auto generated:Getters (SeePrivate/Public Attributes)Setters (SeePrivate/Public Attributes)Constructor (SeeConstructor)A better inheritance system (including abstract, interfaces. SeeInheritance)Lazy attributes (SeeLazy Attributes)Enforcement of:Attribute type - IncludingRecursive TypesBETA(types as strings)Inheritance:Abstract class/interface cannot be createdAbstract methods implementedExistance of attributesAttribute modifiers (before/around/after) available for every:AttributeMethodSome of Python native methods, such as:__init__(SeePython special methods)__new__(SeePython special methods)Static methods (SeeStatic Methods)Static attributes (Seekisa.StaticInfo)Kisa is designed to make classes in Python faster to write, maintain, better organized, and safer.InstallIn order to install Kisa, simply run the following command:pipinstallkisaImportimportkisaCreate class:The only thing required in order to create Kisa class is to pass themetaclass=kisa.Classargument at class creationimportkisaclassEmptyClass(metaclass=kisa.Class):passClass Attributes:In Kisa, unlike Python native OOP, attributes are defined at class decleration and not during runtime. This cannot be modified.Kisa has 2 type of attributes:kisa.Info - Describes regular attributes. Seekisa.Infokisa.StaticInfo - Describes static attributes. Seekisa.StaticInfoKisa attribute decleration is as follows:classPerson(metaclass=kisa.Class):amount_created=kisa.StaticInfo(final=False,type=int,default=0)firstname=kisa.Info(final=False,type=str,required=True)lastname=kisa.Info(final=False,type=str,required=True)birth_country=kisa.Info(final=True,type=str,required=True)# Otherwise all objects would share the same listfriends=kisa.Info(final=True,type=list,required=False,default=lambda:[])age=kisa.Info(final=False,type=int,required=True,allow_none=False)kisa.Info:kisa.InfoClass is used to define attribute parameters:final- Is variable final, i.e. cannot be modified. (defaultFalse)type- Type of the attribute (defaultobject). For recursive type (class A has type of class A) seeRecursive TypesBETArequired- Is attribute required to be passed to constructor. (defaultTrue)default- Default value - Note that if value iscallable(i.e. a function/lambda) the default value will be the return value of the function (defaultNone). Seedefaultallow_none- Could the attribute be None (defaultTrue) - IfFalse, it will raise Exception when trying to set the attribute value toNonelazy- Is attribute is lazy (defaultFalse) - IfTrue, its value will be assigned only when it's value is required (SeeLazy Attributes)defaultWhen declaring new attribute in Kisa, we pass thedeafultattribute to default the attribute value to anything we'd like. It can be:Primitive (e.g.stringorint)Reference (e.g.list,dictor some object)This option is highly discouraged! uselambda expressioninsteadNOTE: if you'll pass a reference, it will be shared with all of the class objects.Lambda expression - Seedefaultlambda expressionargumentdefault- lambda expressionKisa automatically detectscallabaleobjects (i.e. method/lambda),
calls them and the attribute value to the returned value.classSchoolClass(metaclass=kisa.Class):# NOTE: If we didnt use lambda, all objects would point to the same liststudent_list=kisa.Info(default=lambda:[])Say we want to default to a method rather then a lambda (perhaps since we require more complicated initialization process).
It can be problematic to reference to a method since the method will only be declared later.For instance, the following willNOTwork:# This will NOT workclassLogger(metaclass=kisa.Class):# Will raise Exception stating "init_log_file" does not existlog_file=kisa.Info(default=init_log_file)definit_log_file(self):file_path="/var/log/my_logger.log"ifnotself.file_exists(file_path):self.create_file(file_path)returnself.open_file(file_path,mode="w")# Rest of the code# ...Kisa can pass to default methods either 0 or 1 parameters, depends on what the method expects to receive:0 paramters - Kisa will pass no parameters to the method1 parameters - Kisa will pass the self object to the methodGiven that. We can accomplish multi-line initialization by using lambda with self parameter, and from there calling the initialization methodExample:# This will workclassLogger(metaclass=kisa.Class):# 0 parametersdebug_level=kisa.Info(default=lambda:"production")# 1 parameterslog_file=kisa.Info(default=lambdaself:self.init_log_file())definit_log_file(self):file_path="/var/log/my_logger.log"ifnotself.file_exists(file_path):self.create_file(file_path)returnself.open_file(file_path,mode="w")# Rest of the code# ...Attributes Assignment OrderAnother thing that might prove useful, is using an attribute value during another attribute default initialization. Kisa can handle those requests and will handle those requests and will assign attributes values in the order they are required by the default valuesExample:classA(metaclass=kisa.Class):a=kisa.Info(final=True,default=lambdaself:self.b()+self.d()+1)b=kisa.Info(final=True,default=lambdaself:self.c()+1)c=kisa.Info(final=True,default=0)d=kisa.Info(final=False,required=True)a=A(d=3)print(a.a())# prints 5print(a.b())# prints 1print(a.c())# prints 0In this example, the execution order is as follows:dis assigned - User provided values are always assigned first.cis assigned - Required byb.bis assigned - Required bya.ais assigned - Nobody requires it.If we'll take a look at the logger we've shown before, it will prove useful:classLogger(metaclass=kisa.Class):debug_level=kisa.Info(type=str,default="production")file_path=kisa.Info(type=str,default="/var/log/my_logger.log")log_file=kisa.Info(default=lambdaself:self.init_log_file())definit_log_file(self):ifnotself.file_exists(file_path.file_path()):self.create_file(file_path.file_path())self.write_log("Initialized Log File")returnself.open_file(file_path.file_path(),mode="w")defwrite_log(self,msg_log_level,msg):ifmsg_log_level==self.debug_level():print(msg)# Rest of the code# ...The example would first createfile_path, thendebug_leveland only then it will completelog_file. This example is safe to use and the assignment order is enforcedStatic Info -kisa.StaticInfoSeeStatic Attributes - kisa.StaticInfoLazy AttributesA lazy attribute is an attribute which his value is retreived only when required and not at construction. One instance when this is useful is when an attribute usage is questionable, but also expensive to retrieve, so we'll avoid it until we must get it.Lazy attribute works exactly like regular attribute, but its value will be calculated when:Using the attribute getter for the first time - Its value will be the value retrieved fromdefault.Its value was given from the constructor - In this case, the default value will NOT be calculated.Its value was given from the setter - In this case, the default value will NOT be calculated.Examples:classDataProcessor(metaclass=kisa.Class):file_name=kisa.Info(type=str,allow_none=False,required=True)processed_data=kisa.Info(type=str,allow_none=False,default="get_data",lazy=True)defget_data(self):print(f"Getting data for{self.file_name()}")withopen(self.file_name())asf:data=f.read()# do some processing on the data# ...returnprocessed_datadefpost_data(self,data):# Post the data online# ...# NOTE: This file is very very large! (1~2gb)data_processor=DataProcessor(file_name="/var/log/app.log")# do some actions...# Only now it will get processed_data value and would print its logpost_data(data_processor.processed_data())Constructor:Kisa automatically generates the constructor by itself:classPerson(metaclass=kisa.Class):firstname=kisa.Info(final=False,type=str,required=True)lastname=kisa.Info(final=False,type=str,required=True)favorite_food=kisa.Info(final=False,type=str,required=True)friends=kisa.Info(final=True,type=int,required=False,default=lambda:[])# NOTE: friends is generated automaticallymyself=Person(firstname="Noam",lastname="Nis",favorite_food="Pizza")Note: It is possible to modify the construction process, as explained in theOverriding constructorsectionRecursive Types -BETAIMPORTANT- Recursive Types is inbetaand might not detect the classes.A common practice when writing classes is to use recursive types, i.e. having classes requiring themselves (classAhas attribute of typeA).
Another issue that might arise is the usage of a class that will only be declared later in the file.Example (The following code will not work):# Example 1 - Will not workclassA(metaclass=kisa.Class):a=kisa.Info(type=A)# Example 2 - Will not workclassA(metaclass=kisa.Class):b=kisa.Info(type=B)classB(metaclass=kisa.Class):a=kisa.Info(type=A)# Example 3 - Will not work (class B doesn't exist at time of creating class A)classA(metaclass=kisa.Class):b=kisa.Info(type=B)classB(metaclass=kisa.Class):passIn order to solve this issue, Kisa supports to pass types by string and not by reference.Kisa is designed to handle:local package typeother package typeA couple examples:# Example 1classA(metaclass=kisa.Class):a=kisa.Info(type="A")# Example 2classA(metaclass=kisa.Class):b=kisa.Info(type="B")classB(metaclass=kisa.Class):a=kisa.Info(type=A)# Example 3classA(metaclass=kisa.Class):b=kisa.Info(type="B")classB(metaclass=kisa.Class):passThis will also work:classPerson(metaclass=kisa.Class):fullname=kisa.Info(type="str",required=True)age=kisa.Info(type="int",required=True)NOTE- When passing type as a string, the type validation will only occur when trying to create the first instance, not beforePrivate/Public Attributes:In Kisa, all attributes are Private and can only be accessed via Get/Set method.
Those methods are generated automatically, and can be modified.
This is explained with greater detail in theCustom getter/settersection.getter/setter:NOTE: setter also returns the value it has setUse as follows:myself=Person(firstname="Noam",lastname="Nis",favorite_food="Pizza")myself.firstname()# Getter (returns "Noam")myself.favorite_food("Burger")# Setter (returns "Burger")Please note that the get/set methods are actually the same method but passed with different number of parameters:When called with 1 parameter - it would be treated as a setter method.When called with no parameters - it will be treated as a getter method.Custom getter/setter:We can also override Kisa default getter/setter by using the decorator@kisa.getter('attribute_name')- expects to decorate a function that receivesselfand the attribute valueclassPerson(metaclass=kisa.Class):firstname=kisa.Info(final=False,type=str,required=True)@kisa.getter('firstname')defget_firstname(self,firstname_value):returnf"Mr.{firstname_value}"noam_person=Person(firstname="Noam")dani_person=Person(firstname="Dani")print(noam_person.firstname())# prints "Mr Noam"print(dani_person.firstname())# prints "Mr Dani"@kisa.setter(attribute_name)- expects to decorate a function that receivesselfand the new value to be setNote: The value that is set to the attribute at the end is the value that is returnedclassEmployee(metaclass=kisa.Class):salary=kisa.Info(final=False,type=str,required=True)@kisa.setter('salary')defset_salary(self,new_salary):ifnew_salary<=0:raiseException("Salary must be bigger then 0!")returnnew_salarynoam=Employee(salary=15)noam.salary(-30)# Raises exception: Salary must be bigger then 0!Attribute Modifier:Attribute Modifier is a way to redirect an attribute/function call and maniputlate it, Kisa has 3 Attribute Modifiers:before - Called before every call to the attribute/functionReceives:selfattribute name*args**kwargsaround - Called to surround every call to the attribute/function, and can decide if it wants to call the function at all and with what parameters (seeAround Modifier)Receives:selfattribute namenext call - Receives*args**kwargsafter - Called after every call to the functionReceives:selfattribute name*args**kwargsAround ModifierAround Modifier allows us to manipulate the method call:Decide what args goes to the original method - The args passes tonext_callmethodWhat value the original method will finally return - The value returned from thearoundmethodCall Order:all before methods, same order in codeall around methods, same order in code (this includes the method call itself)all after methods, same order in codeExample:classSound(metaclass=kisa.Class):# ...defplay(self,filename):# plays a sound from filepass@kisa.before("play","play_reverse")defplay_intro_sound(self,attr_name,filename):# plays intro sound before every sound playpass@kisa.around("play","play_reverse")defvalidate_sound_exists(self,attr_name,next_call,filename):# if file exists, play normally, otherwise play an error soundifself.file_exists(filename):returnnext_call(filename)else:returnnext_call("error_sound.mp3")@kisa.after("play","play_reverse")defplay_exit_sound(self,attr_name,filename):# plays exit sound after every sound playpassNOTE:the attribute modifier itself, which is a function won't exist in the class/objectsclassModifierClass(metaclass=kisa.Class):deffoo(self):pass@kisa.before("foo")defbar(self):passobj=ModifierClass()obj.bar()# Throws exception, bar doesn't existModifying multiple attributes at onceWe can pass before/around/after a list of attributes, kisa will hook to all of the attributes in the listExample:The following example shows part of a logger system that creates a log file for each log levelimportosclassLogger(metaclass=kisa.Class):info_file=kisa.Info(type=str,required=True,allow_none=False,final=False)warn_file=kisa.Info(type=str,required=True,allow_none=False,final=False)error_file=kisa.Info(type=str,required=True,allow_none=False,final=False)@kisa.before("info_file","warn_file","error_file")defcreate_if_not_exists(self,attr_name,*args):iflen(args)==0:# Getter, skipreturnelse:# Setterfile_name=args[0]ifnotos.path.exists(file_name):# Create fileopen(file_name,"w").close()# Creates 3 files: ./info.txt ./warn.txt ./error.txtlogger=Logger(info_file="./info.txt",warn_file="./warn.txt",error_files="./error.txt")Python special methods (__init__,__setattr__,__getattribute__,__call__, etc...)Kisa supports Attribute Modifiers for Python native methods. Currently supports:__init__: Allows you to modify the constructor, or run your own things - seeOverriding constructorfor more details__new__: Allows you to modify the__new__call during object creation, in order to modify it (SeeSingletonfor example)Static attributes/methodsStatic Attributes -kisa.StaticInfoIn order to declare static attributes we usekisa.StaticInfo.kisa.StaticInfobehaves exactly likeRegular Attributes (kisa.Info)except for 2 differences:required- isNOTavailable for static attributes. This is so since static attributes are not constructed.default- lambdacan get only 0attributes.Example:classMale(metaclass=kisa.Class):nickname=kisa.StaticInfo(final=True,type=str,default=lambda:"Mr.")print(Male.nickname())# prints "Mr."male_obj=Male()print(male_obj.nickname())# prints "Mr."male_obj.nickname("Sir")# We can modify static attributes from instancesprint(male_obj.nickname())# prints "Sir"print(Male.nickname())# prints "Sir"Static Methods -@kisa.staticIn order to define static methods, simply use the@kisa.staticdecorator and Kisa will create a static method itselfExample:classMath(metaclass=kisa.Class):@kisa.staticdefadd(a,b):returna+bprint(Math.add(1,2))# prints 3my_math=Math()print(my_math.add(1,2))# prints 3StaticModifiersStatic Modifiers, work exactly as regularAttribute Modifiers.
Static Modifiers are automatically detected and act as static methods themselvesExample:classMath(metaclass=kisa.Class):@kisa.staticdefadd(a,b):returna+b@kisa.before("add")defbefore_add(a,b):print("Calling a+b")classPerson(metaclass=kisa.Class):amount_created=kisa.StaticInfo(final=True,type=int,default=lambda:0)@kisa.before("amount_created")defbefore_amount_changed(new_val=None):print(f"New val ={new_val}")Person.amount_created(1)SingletonWe can useStaticInfoin order to create Singleton:classLogger(metaclass=kisa.Class):_initialized=kisa.StaticInfo(type=bool,default=False,allow_none=False)# NOTE: Why we use lazy is explained after the example_singleton=kisa.StaticInfo(type="Logger",lazy=True)name=kisa.Info(type=str)@kisa.around('__new__')defget_singleton(cls,attr_name,next_call,*args,**kwargs):ifLogger._singleton()isNone:Logger._singleton(next_call(*args,**kwargs))returnLogger._singleton()@kisa.around('__init__')definit(self,attr_name,next_call,*args,**kwargs):ifself._initialized()isNone:next_call(*args,**kwargs)self._initialized(True)l1=Logger(name="NoamLogger")l2=Logger()print(l1==l2)# Prints "True"Why_singletonislazy?The reason lies in the fact that static attributes values are computed during class creation.
That means that by that time there is noLoggerclass to be found.
When_singletonislazy, kisa will look for typeLoggeronly when we'll assign a value to it.
By that timeLoggerwill already be defined.Inheritance:Inheritance works the same as in python OOP:classVehicle(metaclass=kisa.Class):wheels_amount=kisa.Info(type=int,final=True)defdrive(self,dest):print(f"Driving to:{dest}with{self.wheels_amount()}wheels")classCar(metaclass=kisa.Class,extends=Vehicle):defdrive(self):print("A car driving...")self._super().wheels_amount()NOTE:the nativesuper()``` method does not work in Kisa, use instead ```self._super()insteadKisa does not support Python native inheritance, only 1 inheritance of class is allowed, viaextends=<extended_class>as seen in the exampleAbstract ClassKisa supports abstract classesclassShape(metaclass=kisa.AbstractClass):@kisa.abstractdefget_circumference(self):passclassQuadrangle(metaclass=kisa.AbstractClass,extends=Shape):a=kisa.Info(type=int,required=True)b=kisa.Info(type=int,required=True)c=kisa.Info(type=int,required=True)d=kisa.Info(type=int,required=True)defget_circumference(self):returnself.a()+self.b()+self.c()+self.d()classRectangle(metaclass=kisa.Class,extends=Quadrangle):passInterfaceKisa supports interfacesThere can be multiple interfaces in a single class/interfaceInterface can only contain@kisa.abstractmethods (no attributes/implementations)classSavable(metaclass=kisa.Interface):@kisa.abstractdefsave():passclassLoadable(metaclass=kisa.Interface):@kisa.abstractdefload():pass# A safe class is a class that can be saved/loaded to/from hard diskclassISafeClass(metaclass=kisa.Interface,implements=[Savable,Loadable]):passclassModifiableConfiguration(metaclass=kisa.Class,implements=ISafeClass):defsave(self,path):# TODO: Save to pathpassdefload(self,path):# TODO: Load object from pathpassNote: For single implemented interface, it is not required to be passed as listNote: Kisa currently does not support method signature enforcementOverriding constructorNOTE:Even though the following example uses inheritance, you are by no means required to use inheritance in order to override the constructorSay we want Rectangle to receive only argumentsaandb, and set the argumentscanddto equalaandbaccordingly. we can achieve this by applyingkisa.aroundto__init__constructor.This in turn will enable you to intercept the call to the constructor before it is called, make your own changes and then let the process proceed normally.classShape(metaclass=kisa.AbstractClass):@kisa.abstractdefget_circumference(self):passclassQuadrangle(metaclass=kisa.AbstractClass,extends=Shape):a=kisa.Info(type=int,required=True)b=kisa.Info(type=int,required=True)c=kisa.Info(type=int,required=True)d=kisa.Info(type=int,required=True)defget_circumference(self):returnself.a()+self.b()+self.c()+self.d()classRectangle(metaclass=kisa.Class,extends=Quadrangle):@kisa.around("__init__")defaround_init(self,attr_name,next_call,a:int,b:int):# NOTE: Since Python __init__ doesn't return anything,#       we are not required to return the value of next(...)next_call(a=a,b=b,c=a,d=b)# Calls Kisa internal constructor# r = Rectangle(a=1, b=2, c=3, d=4) - Throws Exception, expects only params 'a' and 'b'r=Rectangle(a=1,b=2)# a=1, b=2, c=1, d=2A full example:importkisaclassSavable(metaclass=kisa.Interface):@kisa.abstractdefsave():passclassLoadable(metaclass=kisa.Interface):@kisa.abstractdefload():pass# A safe class is a class that can be saved/loaded to/from hard diskclassISafeClass(metaclass=kisa.Interface,implements=[Savable,Loadable]):passclassShape(metaclass=kisa.AbstractClass,implements=ISafeClass):@kisa.abstractdefget_circumference():pass@kisa.abstractdefget_area():passdefload(self):print("Loading...")print("Loaded successfully")defsave(self):print("Saving...")print("Saved successfully")classQuadrangle(metaclass=kisa.AbstractClass,extends=Shape):a=kisa.Info(type=int,required=True)b=kisa.Info(type=int,required=True)c=kisa.Info(type=int,required=True)d=kisa.Info(type=int,required=True)defget_circumference(self):returnself.a()+self.b()+self.c()+self.d()classRectangle(metaclass=kisa.Class,extends=Quadrangle):@kisa.around("__init__")defaround_init(self,attr_name,next_call,a:int,b:int):next_call(a=a,b=b,c=a,d=b)defget_self(self):returnf"{self.a()}x{self.b()}"defget_area(self):returnself.a()*self.b()defdescribe(self,desc):returnf">>{desc}"r=Rectangle(a=3,b=2)# Prints 10print(r.get_circumference())# Prints 6print(r.get_area())# Prints Saving...#        Saved successfullyr.save()# Prints Loading...#        Loaded successfullyr.load()AuthorNoam Nisanov -noam.nisanov@gmail.com