Finistere/antidote

Metaclass issues

Closed this issue · 3 comments

Hello,

I don't understand your decision to inherite from Service instead of using implements/register decorators of previous versions.
Because now if you want to declare an ABC Class for your Service definition, you can't use it alongside Service, as both have metaclasses.

class MyServiceInterface(abc.ABC):
    @abc.abstractmethod
    def do_something(self):
        pass

class MyserviceImpl(MyServiceInterface, antidote.Service):
    def do_something(self):
        print('Just do it')

This result in metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases.

Thanks for your help,

Hello!

Currently, you can solve it in two ways:

  1. Use the @service decorator designed explicitly for that reason when inheriting Service is cumbersome. It's very similar to the old @register, expect it won't wire the class for you. You have to manually use @inject or @wire.
import abc
from antidote import service


class MyServiceInterface(abc.ABC):
    @abc.abstractmethod
    def do_something(self):
        pass

@service
class MyserviceImpl(MyServiceInterface):
    def do_something(self):
        print('Just do it')
  1. You can do the usual trick to solve metaclass conflicts. A bit hackier though, as it's not part of the public API of Antidote:
import abc

from antidote import Service
from antidote._service import ServiceMeta


class MyServiceInterface(abc.ABC):
    @abc.abstractmethod
    def do_something(self):
        pass


class ABCServiceMeta(abc.ABCMeta, ServiceMeta):
    pass


class MyserviceImpl(Service, metaclass=ABCServiceMeta):
    def do_something(self):
        print('Just do it')

Now, why did I change the API?

  • One of the primary goals of Antidote is to enforce maintainable code, in the sense of explicit code, whenever possible. Using metaclasses instead of decorator forces the declaration of Antidote's configuration to be where the class is actually defined. With a decorator you can apply it somewhere else, it's easier to do magic stuff that hinders maintainability. As the @service decorator exists as an escape hatch, I find it IMHO as flexible.
  • Metaclasses provide a better typing experience for the dependency @ factory and dependency.parameterized(...) syntax.
  • The wiring of classes is cleaner, at least for me as a maintainer, with a nested configuration class than class decorators. It required having to copy-paste several arguments and their documentation and the API design was worse IMHO.

Regarding the old @implements decorator. It has been replaced by the more versatile and more explicit @implementation. With @implements it wasn't obvious from where the implementation would be coming from and if Python had loaded the file before.

API ref: https://antidote.readthedocs.io/en/latest/reference.html#module-antidote.implementation
Interface example: https://antidote.readthedocs.io/en/latest/recipes.html#use-interfaces

I'll add

  • a note in the Service docstring (API Reference). It's obviously missing
  • a recipe in the documentation for it.

As creating the ABCServiceMeta isn't obvious, I'm considering creating a ABCService that would do this for you. It'd be just easier and serve as documentation on how to handle this with metaclasses if features of ServiceMeta are needed.

Does this solve your issue ? Do you see anything else that could be improved?

Hello,

Thank you so much for your awesome response !
Indeed @service is solving my problem :)

Glad it helped! :)

Just FYI I've fixed the documentation and the readme to be clearer on that issue:
https://antidote.readthedocs.io/en/latest/recipes.html#resolve-metaclass-conflict-with-service

I've also added ABCService as part of the public API.

Closing the issue as your issue is solved.