Or how to design an abstract class A such that A() or A.anything throws 'dependency missing' error but when inheriting like B(A), A behaves like a usual abc.ABC class?
In one of my software projects I handle quite a few of optional dependencies.
The workflow is as follows:
- Where the optional dependency is needed create a variable that is set with an abstract Interface class.
- Create a optional dependency class inheriting from the Interface which directly imports the dependency.
- At runtime override the variable from 1. with either the optional dependency class directly or an instance of it.
Now, if for some reason the optional dependecy could not be loaded and the program tries to use the Interface from 1. directly, I want there to be an error message stating something like:
'Optional Dependecy <name> was not injected. Is <name> installed?'
By 'try to use' I mean either trying to instantiate or accessing its attributes.
I wanted to achieve this by designing a custom class (InterfaceTemplate) that acts like a normal abc.ABC except if you directly inherit from it (by which I mean that __mro__[1] == InterfaceTemplate).
My solution is given below. I don't really like it since it involves permanently modifying the __getattribute__ functions for all child classes not only for ones that directly inherit form the InterfaceTemplate.
Is there a better solution?
Am I reinventing the wheel because it feels like this would be something useful?
Or is what I am trying to do bad in some way?
Now for my solution
import abc
class TemplateMeta(abc.ABCMeta):
def _is_optional_dependency_interface(cls):
return (cls.__mro__[1].__name__=='InterfaceTemplate')
def __call__(cls,*args,**kwargs):
if cls._is_optional_dependency_interface():
raise AssertionError(f'{cls._dependency_}')
instance = super().__call__(*args,**kwargs)
return instance
def __getattribute__(cls,key):
if key[0]!='_':
if cls._is_optional_dependency_interface():
raise AssertionError(f'{cls._dependency_}')
return super().__getattribute__(key)
class InterfaceTemplate(abc.ABC,metaclass=TemplateMeta):
_dependency_='default'
class Interface(InterfaceTemplate):
_dependency_='dependency-v1.0'
@abc.abstractmethod
def fu():
pass
class Dependency(Interface):
def __init__(self):
super().__init__()
self.a=2
def fu(self):
pass
class DependencyWrong(Interface):
def __init__(self):
super().__init__()
self.a=2
def no_fu(self):
pass
This results in the following behaviour:
>>> Interface()
AssertionError: dependency-v1.0
>>> Interface.fu
AssertionError: dependency-v1.0
>>> Interface.non_existing_attribute
AssertionError: dependency-v1.0
>>> Dependency()
<__main__.Dependency object at 0x7fb9eb3a8690>
>>> Dependency.fu
<function Dependency.fu at 0x7fb9eb2d5e40>
>>> Dependency.non_existing_attribute
AttributeError: type object 'Dependency' has no attribute 'non_existing_attribute'
>>> DependencyWrong()
TypeError: Can't instantiate abstract class DependencyWrong with abstract method fu
Only accessing attributes of Interface that start with an '_' would not lead to an error. I didn't figure out how to also exclude those without causing trouble in the class creation (calls to __new__ and the like) but that is not important.