0

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:

  1. Where the optional dependency is needed create a variable that is set with an abstract Interface class.
  2. Create a optional dependency class inheriting from the Interface which directly imports the dependency.
  3. 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.

Tim
  • 111
  • 6
  • 1
    _"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"_ ...this is how an ABC abstract class would behave anyway. Any class that inherits from ABC but doesn't yet implement the abstract methods will raise an error when you try to instantiate it. And you can use `@abstractproperty` to define 'abstract attributes' (see https://stackoverflow.com/a/42529760/202168) – Anentropic Aug 15 '23 at 09:58
  • I'm finding it hard to understand on a practical level what you're trying to achieve (I mean the wider context, outside of the solution you've chosen and are trying to implement) – Anentropic Aug 15 '23 at 10:00
  • 1
    There are a handful of DI frameworks for Python already, one I've used a few times myself is https://github.com/ivankorobkov/python-inject – Anentropic Aug 15 '23 at 10:01
  • @Anentropic A normal ABC would not complain when one tries to access its attributes and it would only fail to instantiate if it actually has abstract methods. Thanks for the tip with python-inject. The wider scope is just that I want my program/library to fail in a safe way with meaningful error messages when someone is trying to access Interfaces that are not replaced by the dependency injection. – Tim Aug 15 '23 at 10:14
  • 1
    if you define all the abstract attributes as `@abstractproperty` and `raise NotImplementedError` from the abstract definition then it would behave how you want I think – Anentropic Aug 15 '23 at 14:40
  • 1
    Agree 100% with @Anentropic above, with the slight nitpick that `@abstractproperty` is now deprecated and replaced by `@property` `@abstractmethod` (in that order), instead. Either works currently, but the former is going away at some point. – BadZen Aug 27 '23 at 17:06

0 Answers0