ivankorobkov / python-inject

Python dependency injection
Apache License 2.0
671 stars 77 forks source link

inject.attr Incompatible Python 3.8 #77

Open andrewborba10 opened 3 years ago

andrewborba10 commented 3 years ago

Hello,

Due to a change in the Mock implementation in Python 3.8, inject.attr is basically busted. The change is this: when a spec is passed to a Mock, an iteration happens on all the classes's attributes, and getattr() is called for each one. Something like (other details are intentionally obfuscated there):

for attr in dir(spec_class):
    getattr(attr)

Since it's normal to create mocks during the configuration step (inside the config function used in inject.configure()), this triggers InjectionException because when getattr() hits an attribute with an _AttributeInjection as its value, it tries to call instance() for that binding -> crash.

My team looked into some solutions, the best one we came up with is a change in the inject framework, as anything else caused a lot of disruption in our codebase. The goal of my change was to delay the attribute error until the injected attribute is actually used. This can be achieved by simply injecting a None instead of raising an exception, but instead I return some sentinel class that will fail if ever called on. Here is the proposed change:

class _NullInjectedAttribute(object):
    """
    Returned for injected attributes that don't have an injection configuration.
    """
    pass

class _AttributeInjection(object):
    def __init__(self, cls):
        self._cls = cls

    def __get__(self, obj, owner):
        # This is a patch to solve an issue that arose in Python 3.8.
        # The base Mock class now iterates through all attributes and
        # calls getattr() in a class that is used in the `spec` field
        # for a Mock. This triggers this codepath and causes injection
        # failures if any mock is initialized before the graph is configured.
        try:
            return instance(self._cls)
        except InjectorException:
            return _NullInjectedAttribute()

It's a bit crude, but gets the job done. I'm wondering if you agree with this general approach, I'd be happy to submit a PR if so.

andrewborba10 commented 3 years ago

https://github.com/ivankorobkov/python-inject/pull/78