Package: objectextensions
Object ExtensionsA basic framework for implementing an extension patternSummaryThe point of this framework is to provide a more modular alternative to object inheritance.Consider the following use case:You have an abstract classCarintended to represent a car, and need a pattern that allows you tooptionallyadd more features.
For example, you may want to add a convertible roof or a touchscreen on the dashboard, but these features will not necessarily be added to every subclass ofCaryou create.Applying standard OOP here means you would need to make a subclass every time a new combination of these optional features is needed.
In the above case, you may need one subclass for a car with a convertible roof, one subclass for a car with a touchscreen, and one that has both features. As the amount of optional features increases,
the amount of possible combinations skyrockets. This is not a scalable solution to the problem.Object Extensions is an elegant way to handle scenarios such as this one. Rather than creating a new subclass for each possible combination,
you create one extension representing each feature. When you need to create a car with a particular set of features,
you take the parent class and pass it the exact set of extensions you want to apply via the.with_extensions()method as the need arises.Note that this pattern is intended to be used alongside inheritance, not to replace it entirely. The two can be mixed without issue, such that
(for example) a subclass could extend a parent class that has pre-applied extensions like so:classSpecificCarModel(Car.with_extensions(TouchscreenDash)):passQuickstartSetupBelow is an example of an extendable class, and an example extension that can be applied to it.fromobjectextensionsimportExtendableclassHashList(Extendable):"""A basic example class with some data and methods.Inheriting Extendable allows this class to be modified with extensions"""def__init__(self,iterable=()):super().__init__()self.values={}self.list=[]forvalueiniterable:self.append(value)defappend(self,item):self.list.append(item)self.values[item]=self.values.get(item,[])+[len(self.list)-1]defindex(self,item):"""Returns all indexes containing the specified item.Much lower time complexity than in a typical list due to dict lookup usage"""ifitemnotinself.values:raiseValueError(f"{item}is not in hashlist")returnself.values[item]fromobjectextensionsimportExtensionclassListener(Extension):"""This extension class is written to apply a counter to the HashList class,which increments any time .append() is called"""@staticmethoddefcan_extend(target_cls):returnissubclass(target_cls,HashList)@staticmethoddefextend(target_cls):Extension._set(target_cls,"increment_append_count",Listener.__increment_append_count)Extension._wrap(target_cls,"__init__",Listener.__wrap_init)Extension._wrap(target_cls,'append',Listener.__wrap_append)def__wrap_init(self,*args,**kwargs):Extension._set(self,"append_count",0)yielddef__wrap_append(self,*args,**kwargs):yieldself.increment_append_count()def__increment_append_count(self):self.append_count+=1InstantiationHashListWithListeners=HashList.with_extensions(Listener)my_hashlist=HashListWithListeners(iterable=[5,2,4])or, for shorthand:my_hashlist=HashList.with_extensions(Listener)(iterable=[5,2,4])Result>>>my_hashlist.append_count# Attribute that was added by the Listener extension3>>>my_hashlist.append(7)# Listener has wrapped this method with logic which increments .append_count>>>my_hashlist.append_count4FunctionalityPropertiesExtendable.extensionsReturns a reference to a tuple containing any applied extensions.Extendable.extension_dataReturns a snapshot of the instance's extension data.This is intended to hold metadata optionally provided by extensions for the sake of introspection,and for communication between extensions.MethodsExtendable.with_extensions(cls, *extensions: Type[Extension])Returns a subclass with the provided extensions applied to it.Extension.can_extend(target_cls: Type[Extendable])Abstract staticmethod which must be overridden.Should return a bool indicating whether this Extension can be applied to the target class.Extension.extend(target_cls: Type[Extendable])Abstract staticmethod which must be overridden.Any modification of the target class should take place in this function.Extension._wrap(target_cls: Type[Extendable], method_name: str,gen_func: Callable[..., Generator[None, Any, None]])Used to wrap an existing method on the target class.Passes copies of the method parameters to the generator function provided.The generator function should yield once,with the yield statement receiving a copy of the result of executing the core method.Extension._set(target: Union[Type[Extendable], Extendable], attribute_name: str, value: Any)Used to safely add a new attribute to an extendable class.Note: It is possible but not recommended to modify an instance rather than a class using this method.Will raise an error if the attribute already exists (for example, if another extension has already added it)to ensure compatibility issues are flagged and can be dealt with easily.Extension._set_property(target: Union[Type[Extendable], Extendable], property_name: str, value: Callable[[Extendable], Any])Used to safely add a new property to an extendable class.Note: It is possible but not recommended to modify an instance rather than a class using this method.Will raise an error if the attribute already exists (for example, if another extension has already added it)to ensure compatibility issues are flagged and can be dealt with easily.Extension._set_setter(target: Union[Type[Extendable], Extendable], setter_name: str, linked_property_name: str,value: Callable[[Extendable, Any], Any])Used to safely add a new setter to an extendable class.Note: It is possible but not recommended to modify an instance rather than a class using this method.If the property this setter is paired with does not use the same attribute name,and the setter's name already exists on the class (for example, if another extension has already added it),an error will be raised. This is to ensure compatibility issues are flagged and can be dealt with easily.Additional InfoAs extensions do not properly invoke name mangling, adding private members via extensions is discouraged; doing so may lead to unintended behaviour.
Using protected members instead is encouraged, as name mangling does not come into play in this case.