kisa 0.1.0a1

Creator: rpa-with-ash

Last updated:

Add to Cart

Description:

kisa 0.1.0a1

Kisa - Python Object Oriented System
Kisa 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 (See Private/Public Attributes)
Setters (See Private/Public Attributes)
Constructor (See Constructor)


A better inheritance system (including abstract, interfaces. See Inheritance)
Lazy attributes (See Lazy Attributes)
Enforcement of:

Attribute type - Including Recursive Types BETA (types as strings)
Inheritance:

Abstract class/interface cannot be created
Abstract methods implemented


Existance of attributes


Attribute modifiers (before/around/after) available for every:

Attribute
Method
Some of Python native methods, such as:

__init__ (See Python special methods)
__new__ (See Python special methods)


Static methods (See Static Methods)
Static attributes (See kisa.StaticInfo)



Kisa is designed to make classes in Python faster to write, maintain, better organized, and safer.
Install
In order to install Kisa, simply run the following command:
pip install kisa

Import
import kisa

Create class:
The only thing required in order to create Kisa class is to pass the metaclass=kisa.Class argument at class creation
import kisa

class EmptyClass(metaclass=kisa.Class):
pass

Class 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. See kisa.Info
kisa.StaticInfo - Describes static attributes. See kisa.StaticInfo

Kisa attribute decleration is as follows:
class Person(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 list
friends = 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.Info Class is used to define attribute parameters:

final - Is variable final, i.e. cannot be modified. (default False)
type - Type of the attribute (default object). For recursive type (class A has type of class A) see Recursive Types BETA
required - Is attribute required to be passed to constructor. (default True)
default - Default value - Note that if value is callable (i.e. a function/lambda) the default value will be the return value of the function (default None). See default
allow_none - Could the attribute be None (default True) - If False, it will raise Exception when trying to set the attribute value to None
lazy - Is attribute is lazy (default False) - If True, its value will be assigned only when it's value is required (See Lazy Attributes)

default
When declaring new attribute in Kisa, we pass the deafult attribute to default the attribute value to anything we'd like. It can be:

Primitive (e.g. string or int)
Reference (e.g. list, dict or some object) This option is highly discouraged! use lambda expression instead

NOTE: if you'll pass a reference, it will be shared with all of the class objects.


Lambda expression - See default lambda expression

argument default - lambda expression
Kisa automatically detects callabale objects (i.e. method/lambda),
calls them and the attribute value to the returned value.
class SchoolClass(metaclass=kisa.Class):
# NOTE: If we didnt use lambda, all objects would point to the same list
student_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 will NOT work:
# This will NOT work
class Logger(metaclass=kisa.Class):
# Will raise Exception stating "init_log_file" does not exist
log_file = kisa.Info(default=init_log_file)

def init_log_file(self):
file_path="/var/log/my_logger.log"
if not self.file_exists(file_path):
self.create_file(file_path)
return self.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 method
1 parameters - Kisa will pass the self object to the method

Given that. We can accomplish multi-line initialization by using lambda with self parameter, and from there calling the initialization method
Example:
# This will work
class Logger(metaclass=kisa.Class):
# 0 parameters
debug_level = kisa.Info(default = lambda: "production")

# 1 parameters
log_file = kisa.Info(default=lambda self: self.init_log_file())

def init_log_file(self):
file_path="/var/log/my_logger.log"
if not self.file_exists(file_path):
self.create_file(file_path)
return self.open_file(file_path, mode="w")

# Rest of the code
# ...

Attributes Assignment Order
Another 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 values
Example:
class A(metaclass=kisa.Class):
a = kisa.Info(final=True, default=lambda self: self.b() + self.d() + 1)
b = kisa.Info(final=True, default=lambda self: 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 5
print(a.b()) # prints 1
print(a.c()) # prints 0

In this example, the execution order is as follows:

d is assigned - User provided values are always assigned first.
c is assigned - Required by b.
b is assigned - Required by a.
a is assigned - Nobody requires it.

If we'll take a look at the logger we've shown before, it will prove useful:
class Logger(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=lambda self: self.init_log_file())

def init_log_file(self):
if not self.file_exists(file_path.file_path()):
self.create_file(file_path.file_path())
self.write_log("Initialized Log File")
return self.open_file(file_path.file_path(), mode="w")

def write_log(self, msg_log_level, msg):
if msg_log_level == self.debug_level():
print(msg)

# Rest of the code
# ...

The example would first create file_path, then debug_level and only then it will complete log_file. This example is safe to use and the assignment order is enforced
Static Info - kisa.StaticInfo
See Static Attributes - kisa.StaticInfo
Lazy Attributes
A 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 from default.
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:
class DataProcessor(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)

def get_data(self):
print(f"Getting data for {self.file_name()}")
with open(self.file_name()) as f:
data = f.read()

# do some processing on the data
# ...

return processed_data

def post_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 log
post_data(data_processor.processed_data())

Constructor:
Kisa automatically generates the constructor by itself:
class Person(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 automatically
myself = Person(firstname="Noam", lastname="Nis", favorite_food="Pizza")


Note: It is possible to modify the construction process, as explained in the Overriding constructor section

Recursive Types - BETA
IMPORTANT - Recursive Types is in beta and might not detect the classes.
A common practice when writing classes is to use recursive types, i.e. having classes requiring themselves (class A has attribute of type A).
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 work
class A(metaclass=kisa.Class):
a = kisa.Info(type=A)

# Example 2 - Will not work
class A(metaclass=kisa.Class):
b = kisa.Info(type=B)

class B(metaclass=kisa.Class):
a = kisa.Info(type=A)

# Example 3 - Will not work (class B doesn't exist at time of creating class A)
class A(metaclass=kisa.Class):
b = kisa.Info(type=B)

class B(metaclass=kisa.Class):
pass

In order to solve this issue, Kisa supports to pass types by string and not by reference.
Kisa is designed to handle:

local package type
other package type

A couple examples:
# Example 1
class A(metaclass=kisa.Class):
a = kisa.Info(type="A")

# Example 2
class A(metaclass=kisa.Class):
b = kisa.Info(type="B")

class B(metaclass=kisa.Class):
a = kisa.Info(type=A)

# Example 3
class A(metaclass=kisa.Class):
b = kisa.Info(type="B")

class B(metaclass=kisa.Class):
pass

This will also work:
class Person(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 before
Private/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 the Custom getter/setter section.
getter/setter:

NOTE: setter also returns the value it has set

Use 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 receives self and the attribute value

class Person(metaclass=kisa.Class):
firstname = kisa.Info(final=False, type=str, required=True)

@kisa.getter('firstname')
def get_firstname(self, firstname_value):
return f"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 receives self and the new value to be set

Note: The value that is set to the attribute at the end is the value that is returned



class Employee(metaclass=kisa.Class):
salary = kisa.Info(final=False, type=str, required=True)

@kisa.setter('salary')
def set_salary(self, new_salary):
if new_salary <= 0:
raise Exception("Salary must be bigger then 0!")
return new_salary

noam = 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/function

Receives:

self
attribute name
*args
**kwargs




around - 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 (see Around Modifier)

Receives:

self
attribute name
next call - Receives
*args
**kwargs




after - Called after every call to the function

Receives:

self
attribute name
*args
**kwargs





Around Modifier
Around Modifier allows us to manipulate the method call:

Decide what args goes to the original method - The args passes to next_call method
What value the original method will finally return - The value returned from the around method

Call Order:

all before methods, same order in code
all around methods, same order in code (this includes the method call itself)
all after methods, same order in code

Example:
class Sound(metaclass=kisa.Class):
# ...

def play(self, filename):
# plays a sound from file
pass

@kisa.before("play", "play_reverse")
def play_intro_sound(self, attr_name, filename):
# plays intro sound before every sound play
pass

@kisa.around("play", "play_reverse")
def validate_sound_exists(self, attr_name, next_call, filename):
# if file exists, play normally, otherwise play an error sound
if self.file_exists(filename):
return next_call(filename)
else:
return next_call("error_sound.mp3")

@kisa.after("play", "play_reverse")
def play_exit_sound(self, attr_name, filename):
# plays exit sound after every sound play
pass

NOTE: the attribute modifier itself, which is a function won't exist in the class/objects
class ModifierClass(metaclass=kisa.Class):
def foo(self):
pass

@kisa.before("foo")
def bar(self):
pass

obj = ModifierClass()
obj.bar() # Throws exception, bar doesn't exist

Modifying multiple attributes at once
We can pass before/around/after a list of attributes, kisa will hook to all of the attributes in the list
Example:
The following example shows part of a logger system that creates a log file for each log level
import os

class Logger(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")
def create_if_not_exists(self, attr_name, *args):
if len(args) == 0:
# Getter, skip
return
else:
# Setter
file_name = args[0]
if not os.path.exists(file_name):
# Create file
open(file_name, "w").close()

# Creates 3 files: ./info.txt ./warn.txt ./error.txt
logger = 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 - see Overriding constructor for more details
__new__: Allows you to modify the __new__ call during object creation, in order to modify it (See Singleton for example)

Static attributes/methods
Static Attributes - kisa.StaticInfo
In order to declare static attributes we use kisa.StaticInfo. kisa.StaticInfo behaves exactly like Regular Attributes (kisa.Info) except for 2 differences:

required - is NOT available for static attributes. This is so since static attributes are not constructed.
default - lambda can get only 0 attributes.

Example:
class Male(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 instances

print(male_obj.nickname()) # prints "Sir"
print(Male.nickname()) # prints "Sir"

Static Methods - @kisa.static
In order to define static methods, simply use the @kisa.static decorator and Kisa will create a static method itself
Example:
class Math(metaclass=kisa.Class):
@kisa.static
def add(a, b):
return a + b

print(Math.add(1, 2)) # prints 3

my_math = Math()
print(my_math.add(1, 2)) # prints 3

Static Modifiers
Static Modifiers, work exactly as regular Attribute Modifiers.
Static Modifiers are automatically detected and act as static methods themselves
Example:
class Math(metaclass=kisa.Class):
@kisa.static
def add(a, b):
return a + b

@kisa.before("add")
def before_add(a, b):
print("Calling a+b")

class Person(metaclass=kisa.Class):
amount_created = kisa.StaticInfo(final=True, type=int, default=lambda: 0)

@kisa.before("amount_created")
def before_amount_changed(new_val=None):
print(f"New val = {new_val}")

Person.amount_created(1)

Singleton
We can use StaticInfo in order to create Singleton:
class Logger(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__')
def get_singleton(cls, attr_name, next_call, *args, **kwargs):
if Logger._singleton() is None:
Logger._singleton(next_call(*args, **kwargs))
return Logger._singleton()

@kisa.around('__init__')
def init(self, attr_name, next_call, *args, **kwargs):
if self._initialized() is None:
next_call(*args, **kwargs)
self._initialized(True)


l1 = Logger(name="NoamLogger")
l2 = Logger()

print(l1 == l2) # Prints "True"

Why _singleton is lazy?
The reason lies in the fact that static attributes values are computed during class creation.
That means that by that time there is no Logger class to be found.
When _singleton is lazy, kisa will look for type Logger only when we'll assign a value to it.
By that time Logger will already be defined.
Inheritance:
Inheritance works the same as in python OOP:
class Vehicle(metaclass=kisa.Class):
wheels_amount = kisa.Info(type=int, final=True)

def drive(self, dest):
print(f"Driving to: {dest} with {self.wheels_amount()} wheels")

class Car(metaclass=kisa.Class, extends=Vehicle):
def drive(self):
print("A car driving...")
self._super().wheels_amount()

NOTE:

the native super()``` method does not work in Kisa, use instead ```self._super() instead
Kisa does not support Python native inheritance, only 1 inheritance of class is allowed, via extends=<extended_class> as seen in the example

Abstract Class
Kisa supports abstract classes
class Shape(metaclass=kisa.AbstractClass):
@kisa.abstract
def get_circumference(self):
pass


class Quadrangle(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)

def get_circumference(self):
return self.a() + self.b() + self.c() + self.d()


class Rectangle(metaclass=kisa.Class, extends=Quadrangle):
pass

Interface
Kisa supports interfaces

There can be multiple interfaces in a single class/interface
Interface can only contain @kisa.abstract methods (no attributes/implementations)

class Savable(metaclass=kisa.Interface):
@kisa.abstract
def save():
pass

class Loadable(metaclass=kisa.Interface):
@kisa.abstract
def load():
pass

# A safe class is a class that can be saved/loaded to/from hard disk
class ISafeClass(metaclass=kisa.Interface, implements=[Savable, Loadable]):
pass

class ModifiableConfiguration(metaclass=kisa.Class, implements=ISafeClass):
def save(self, path):
# TODO: Save to path
pass

def load(self, path):
# TODO: Load object from path
pass


Note: For single implemented interface, it is not required to be passed as list
Note: Kisa currently does not support method signature enforcement

Overriding constructor

NOTE: Even though the following example uses inheritance, you are by no means required to use inheritance in order to override the constructor

Say we want Rectangle to receive only arguments a and b, and set the arguments c and d to equal a and b accordingly. we can achieve this by applying kisa.around to __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.
class Shape(metaclass=kisa.AbstractClass):
@kisa.abstract
def get_circumference(self):
pass

class Quadrangle(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)

def get_circumference(self):
return self.a() + self.b() + self.c() + self.d()

class Rectangle(metaclass=kisa.Class, extends=Quadrangle):
@kisa.around("__init__")
def around_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=2

A full example:
import kisa

class Savable(metaclass=kisa.Interface):
@kisa.abstract
def save():
pass

class Loadable(metaclass=kisa.Interface):
@kisa.abstract
def load():
pass

# A safe class is a class that can be saved/loaded to/from hard disk
class ISafeClass(metaclass=kisa.Interface, implements=[Savable, Loadable]):
pass

class Shape(metaclass=kisa.AbstractClass, implements=ISafeClass):
@kisa.abstract
def get_circumference():
pass

@kisa.abstract
def get_area():
pass

def load(self):
print("Loading...")
print("Loaded successfully")

def save(self):
print("Saving...")
print("Saved successfully")


class Quadrangle(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)

def get_circumference(self):
return self.a() + self.b() + self.c() + self.d()


class Rectangle(metaclass=kisa.Class, extends=Quadrangle):

@kisa.around("__init__")
def around_init(self, attr_name, next_call, a: int, b: int):
next_call(a=a, b=b, c=a, d=b)

def get_self(self):
return f"{self.a()}x{self.b()}"

def get_area(self):
return self.a() * self.b()

def describe(self, desc):
return f">> {desc}"


r = Rectangle(a=3, b=2)

# Prints 10
print(r.get_circumference())

# Prints 6
print(r.get_area())

# Prints Saving...
# Saved successfully
r.save()

# Prints Loading...
# Loaded successfully
r.load()

Author
Noam Nisanov - noam.nisanov@gmail.com

License

For personal and professional use. You cannot resell or redistribute these repositories in their original state.

Customer Reviews

There are no reviews.