Extended Communication Between Classes
To complement the flexibility offered by the Dynamic Class Design tools, an additional set of tools is required to safely extend communication between classes that may or may not be interconnected.
For example, classes may be dynamically interconnected through:
- Class Builder, where a base class is dynamically connected to parent and component classes to create a new class;
- Dynamic Inheritance, where one class dynamically inherits from another;
- Class Composition, where one class is instantiated as a component of another class; or
- Class Merging, where two or more classes are merged to create a new class.
Considering the optional nature of the interconnect types, the communication mechanism is designed to handle cases where a class may be not (yet) interconnected, suppressing any related exceptions that may occur.
decoratewith
Meta decorator decoratewith can be used to decorate a class method with one or
more chained decorators from other interconnected classes. If any decorator is
not found at runtime, no exception is raised and the class method is executed.
Additionally, the syntax of the dynamic decorators aims to get rid of the
boilerplate for wrapping and returning the decorator code, leaving just the
wrapper's code.
Syntax
@decoratewith(
"decorator1", "decorator2", ...,
method_sub_instance=None,
fallback=None,
disable_property=None
)
def decorated_method(...):
...
Arguments:
-
decorator1: str
Decorator method to decorate "decorated_method". Decorator "decorator1" may or may not exist at "decorated_method" call time: in other words, if "decorator1" is a property ofselfwhen "decorated_method" is invoked, then it is used to decorate "decorated_method", otherwise the method is executed without any decoration.NOTE: Any sub-property of
self, such as methods of component classes, may be also used as decorator: in this case the dot notation can be used to select the path to the decorator method. -
decorator2, ...: str (Optional)
Further decorator methods to optionally decorate method "decorated_method" in chain. All the decorator methods are executed as they were nested one into the other following the order in which they are passed todecoratewith. -
method_sub_instance: str (Optional)
If all the decorator methods have the same initial path (typically if they are all methods of one component class), the initial path in common can be omitted and passed inmethod_sub_instanceto avoid repetitions. -
fallback: Callable (Optional)
If any decorator does not exist at the "decorated_method" call time, then a "fallback" function is called with the same arguments passed to "decorated_method". -
disable_property: str (Optional)
Class property name evaluated at "decorated_method" call time. If it is found to be True, then the application of the decorators is disabled.
If a decorator method is a property of self, then it must follow the syntax:
def decorator(self, func):
...
func(self)
...
Otherwise, if it is a sub-property of a self (e.g., a property of a component
class), then the syntax will be:
def decorator(self, func, decorated_self):
...
func(decorated_self)
...
wherein self refers to the sub-property instance, and decorated_self to the
main class instance.
Decorators from superclasses dynamically added
Arguments of decoratewith are loaded at runtime as properties of the variable
self, enabling dynamic decorators to be implemented using a method from a
parent class dynamically added, which is not possible with static decorators.
from dyndesign import decoratewith, DynInheritance
class Parent:
def decorator(self, func):
print("Beginning of method decoration from Parent.")
func(self)
print("End of method decoration from Parent.")
class Child(DynInheritance):
@decoratewith("decorator")
def mtd(self):
print(f"Method `m` of class `Child`")
child = Child()
child.mtd()
# Method `m` of class `Child`
Child.dynparents_add(Parent)
child.mtd()
# Beginning of method decoration from Parent.
# Method `m` of class `Child`
# End of method decoration from Parent.
In the provided code snippet, the method "mtd" is initially called on the "child" instance before it inherits from the "Parent" class. As a result, the decoration with the "decorator" is silently skipped. In contrast, once the "Parent" class is added as a superclass, the "mtd" method is decorated with the "decorator".
Chain of dynamic decorators
Multiple dynamic decorators are chained as the following:
from dyndesign import decoratewith
class Multi:
@decoratewith("decorator1", "decorator2", "decorator3")
def decorated(self):
print(f"Method `decorated`")
def decorator1(self, func):
print("Beginning of decorator1.")
func(self)
print("End of decorator1.")
def decorator2(self, func):
print("Beginning of decorator2.")
func(self)
print("End of decorator2.")
def decorator3(self, func):
print("Beginning of decorator3.")
func(self)
print("End of decorator3.")
multi = Multi()
multi.decorated()
# Beginning of decorator1.
# Beginning of decorator2.
# Beginning of decorator3.
# Method `decorated`
# End of decorator3.
# End of decorator2.
# End of decorator1.
In this example, decorated and decorator methods share the same class. Python builtin decorators could also be used in this case, but it must be considered that dynamic decorators are easier to implement than builtin ones.
Decorators from component classes
A dynamic decorator can be a method of a component class as well. In case of
dynamic decoration from a sub-instance of self, the instance object of the
decorated method is passed to the decorator as the argument decorated_self.
from dyndesign import decoratewith
class Base:
def __init__(self):
self.comp = Component()
@decoratewith("comp.decorator1", "comp.decorator2")
def m(self):
print("Method `m` of class `Base`")
class Component:
def __init__(self):
self.value = "Initial"
def decorator1(self, func, decorated_self):
print(f"Beginning of method decoration #1 ({self.value=})")
self.value = "Processed"
func(decorated_self)
print("End of method decoration #1")
def decorator2(self, func, decorated_self):
print(f"Beginning of method decoration #2 ({self.value=})")
func(decorated_self)
print("End of method decoration #2")
base = Base()
base.m()
# Beginning of method decoration #1 (self.value='Initial')
# Beginning of method decoration #2 (self.value='Processed')
# Method `m` of class `Base`
# End of method decoration #2
# End of method decoration #1
Decorators from merged classes
Dynamic decorators can be used to decorate a method of a base class with a method of an extension class:
from dyndesign import decoratewith, mergeclasses
class Base:
@decoratewith("decorator")
def m(self):
print(f"Method `m` of class `Base`")
class Ext:
def decorator(self, func):
print("Beginning of method decoration from Ext.")
func(self)
print("End of method decoration from Ext.")
merged = mergeclasses(Base, Ext)()
merged.m()
# Beginning of method decoration from Ext.
# Method `m` of class `Base`
# End of method decoration from Ext.
base = Base()
base.m()
# Method `m` of class `Base`
It is noted that when method "m" is called directly in class "Base" without extending it to "Ext", then the decoration is skipped and the method is executed normally.
Additional parameters can be passed to the decorated and decorator methods:
from dyndesign import decoratewith, mergeclasses
class Base:
@decoratewith("decorator")
def m(self, param):
print(f"Method `m` of class `Base` - {param=}.")
class Ext:
def decorator(self, func, param):
print(f"Beginning of method decoration from Ext - {param=}.")
func(self, param)
print("End of method decoration from Ext.")
merged = mergeclasses(Base, Ext)()
merged.m("BETA")
# Beginning of method decoration from Ext - param='BETA'.
# Method `m` of class `Base` - param='BETA'.
# End of method decoration from Ext.
Overloading dynamic decorators
When merging classes, dynamic decorators with the same name follow the same overload rules as the normal methods, i.e. the rightmost decorators win. If for example class "Ext2" including another instance of decorator "decorator" is merged to the merge chain, the rightmost instance of "decorator" overloads the leftmost:
from dyndesign import decoratewith, mergeclasses
class Base:
@decoratewith("decorator")
def m(self):
print(f"Method `m` of class `Base`")
class Ext:
def decorator(self, func):
print("Beginning of method decoration from Ext.")
func(self)
print("End of method decoration from Ext.")
class Ext2:
def decorator(self, func):
print("Beginning of method decoration from Ext2.")
func(self)
print("End of method decoration from Ext2.")
merged = mergeclasses(Base, Ext, Ext2)()
merged.m()
# Beginning of method decoration from Ext2.
# Method `m` of class `Base`
# End of method decoration from Ext2.
Using all the instances of a decorator
The overload behavior of dynamic decorators can be altered with invoke_all in
the same way as it works with methods. If a decorator name is passed in the
invoke_all list as argument of mergeclasses, then multiple decorator
instances with the same name from different extension classes are applied in
chain, as if they had different names. In the above example, passing
"decorator" in the invoke_all list of mergeclasses results in:
...
merged = mergeclasses(Base, Ext, Ext2, invoke_all=["decorator"])()
merged.m()
# Beginning of method decoration from Ext.
# Beginning of method decoration from Ext2.
# Method `m` of class `Base`
# End of method decoration from Ext2.
# End of method decoration from Ext.
Building decorators at runtime
Dynamic decorators can be also built at runtime. In the example below, decorator "deco" exists nowhere statically, but it is programmatically built by the function "build_decorator":
from dyndesign import decoratewith
def build_decorator(instance):
def decorator(func):
print("Start Decoration")
func(instance)
print("End Decoration")
return decorator
class DecoratedClass:
@decoratewith("deco")
def main_method(self):
print("Main Method")
decorated_class = DecoratedClass()
decorated_class.deco = build_decorator(decorated_class)
decorated_class.main_method()
# Start Decoration
# Main Method
# End Decoration
Decorators with fallback
If a fallback method is provided, then it is executed when a decorator does not exist at the call time of decorated method. For example:
from dyndesign import decoratewith
class Base:
def fallback_method(self):
print(f"Fallback method")
@decoratewith("decorator", fallback=fallback_method)
def m(self):
print(f"Method `m` of class `Base`")
class Ext:
def decorator(self, func):
print("Beginning of method decoration from Ext.")
func(self)
print("End of method decoration from Ext.")
merged = mergeclasses(Base, Ext)()
merged.m()
# Beginning of method decoration from Ext.
# Method `m` of class `Base`
# End of method decoration from Ext.
Base().m()
# Fallback function
# Method `m` of class `Base`
safezone Context Manager
Any function or method that may or may not exist at runtime (e.g., methods from
merged classes) can be invoked from Context Manager safezone in order to
selectively suppress the exceptions possibly raised if the function or method is
not found.
Syntax
with safezone("callable_name1", "callable_name2", ..., fallback=fallback):
...
Arguments:
-
callable_name1, callable_name2, ...: str (Optional)
If one or more callable names are passed as arguments, then the exceptions raised when specific callables are not available at runtime are suppressed in the context. If no callable names are passed, then those exceptions are suppressed for all the callables. -
fallback: Callable (Optional)
If any callable invoked within thesafezonecontext does not exist at the call time, then a "fallback" function is called with the same arguments passed to the missing callable.
Safe zone for functions
If no function name is passed as argument of safezone, then each function in
the safe zone's context is protected; if any function name is passed, the
protection is restricted to the functions having that name. For example,
safezone can be used to safely call functions that may or may not exist at
runtime:
from dyndesign import safezone
def fallback():
print("Fallback function")
def function_a():
print("Function `a`")
with safezone(fallback=fallback):
function_a()
non_existent_function()
# Function `a`
# Fallback function
Safe zone for methods
A further example shows that safezone can be used to safely invoke methods
of classes that may or may not be merged with other classes:
from dyndesign import mergeclasses, safezone
class Base:
def fallback(self):
print("Fallback method")
def m(self, class_desc):
print(f"Method `m` of {class_desc}")
with safezone("optional_method", fallback=self.fallback):
self.optional_method()
class ExtOptional:
def optional_method(self):
print("Optional method from class `ExtOptional`")
merged = mergeclasses(Base, ExtOptional)()
merged.m("merged class")
# Method `m` of merged class
# Optional method from class `ExtOptional`
Base().m("class `Base` standalone")
# Method `m` of class `Base` standalone
# Fallback method
safeinvoke
As an alternative to safezone context manager, safeinvoke API can be used to
safely invoke methods that may or may not exist at runtime.
Syntax
returned_value = safeinvoke("method_name", instance, *args, fallback=fallback, **kwargs)
Arguments:
-
method_name: str
If method "method_name" exists at runtime in "instance", then it is invoked with instance "instance" and further arguments optionally passed. Otherwise, execution proceeds normally without any exception raised. -
instance: object
Class instance that may or may not include the method referenced to with "method_name". -
args (Optional)
Positional arguments passed to "method_name". -
fallback: Callable (Optional)
If "method_name" does not exist at runtime, then a "fallback" function is called with the same arguments passed to "method_name". -
kwargs (Optional)
Keyword arguments passed to "method_name". -
return (Optional)
Value optionally returned by "method_name" if "method_name" exists at runtime,Noneotherwise.
Basic Example
Method "m" of class "Base" of the example in Safe zone for
methods can be rewritten using safeinvoke:
from dyndesign import safeinvoke
...
def m(self, class_desc):
print(f"Method `m` of {class_desc}")
safeinvoke("optional_method", self, fallback=self.fallback)
safesuper method
When Dynamic
Inheritance
is utilized, the usage of the super builtin function in a temporarily
non-inheriting class might result in exceptions. To prevent such exceptions, the
alternative option of employing the safesuper method can be utilized.
Syntax
super_object = self.safesuper(
subclass=Subclass | None,
object_or_type=subclass_instance | None,
mocked_attrs=("mocked_attr_1", "mocked_attr_2", ...),
mocked_methods=("mocked_method_1", "mocked_method_2", ...)
)
Arguments:
-
subclass: type (Optional)
Subclass optionally passed as first argument tosuperbuiltin function. -
object_or_type: type or object (Optional)
Object or class optionally passed as second argument tosuperbuiltin function. -
mocked_attrs: Tuple of str (Optional)
Attribute names that are mocked in case that the corresponding attributes are missing in the superclasses. -
mocked_methods: Tuple of str (Optional)
Method names that are mocked in case that the corresponding attributes are missing in the superclasses. -
return (Optional)
super proxy object returned bysuperbuiltin function if the requested superclasses are included in the superclass set, otherwise an object mocked with themocked_attrs/mocked_methodsif those attributes/methods are missing in the superclass set, otherwiseNone.
Basic Example
Method safesuper must replace super function when a class, that is not yet
included in the superclass set, is to be passed as first argument of super:
from dyndesign import DynInheritance
class A:
def __init__(self):
print("Constructor of Class `A`")
class B:
def __init__(self):
print("Constructor of Class `B`")
class C(DynInheritance):
def __init__(self):
super().__init__()
self.safesuper(A, self).__init__()
# `super(A, self).__init__()` --> TypeError
# because `C` does not inherit from `A` yet, at its first instantiation.
print("Constructor of Class `C`")
C()
# Constructor of Class `A`
C.dynparents_add(A, B)
C()
# Constructor of Class `A`
# Constructor of Class `B`
# Constructor of Class `C`
Mocking methods
If a class is not yet included in the superclass set and the class methods have
to be accessed from the future child class, those methods can be mocked to
prevent AttributeError exceptions:
from dyndesign import DynInheritance
class A:
def mtd(self):
print("Method `mtd` of Class `A`")
class B(DynInheritance):
def mtd(self):
self.safesuper(mocked_methods=("mtd",)).mtd()
print("Method `mtd` of Class `B`")
B().mtd()
# Method `mtd` of Class `B`
B.dynparents_add(A)
B().mtd()
# Method `mtd` of Class `A`
# Method `mtd` of Class `B`
safesuper function
safesuper can be used as a function within any class as well as a method within
classes using Dynamic Inheritance. The only syntax difference lies in the first
two arguments, which are required in safesuper function, while they remain
optional when using it as a method.
Syntax
super_object = self.safesuper(
subclass=Subclass,
object_or_type=subclass_instance,
mocked_attrs=("mocked_attr_1", "mocked_attr_2", ...),
mocked_methods=("mocked_method_1", "mocked_method_2", ...)
)
A detailed description of the arguments can be found in the safesuper method section.
The safesuper function can be utilized in classes serving as bases for
buildclass, as shown in this
example.