python-injector / injector

Python dependency injection framework, inspired by Guice
BSD 3-Clause "New" or "Revised" License
1.27k stars 81 forks source link

`injector.Injector.create_object` on pydantic-settings `BaseSettings` does not without factory function in Injector #253

Open Guibod opened 1 month ago

Guibod commented 1 month ago

As stated in this closed bug at pydantic-settings, i cannot achieve an initialization through injector on python 3.8 and 3.9 (works ok on 3.10+) without a factory method.

The issue was turned down by the pydantic-settings team.

The error message

TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'

Why ?

Pydantic BaseSettings base class uses PEP-604 (str | None) annotation instead of PEP-484 (Union, Optional). It seems that Pydantic BaseSettings class don’t like to be initialized through cls.__new__(cls).

This works

class Configuration(Module):
    def configure(self, binder: Binder):
        binder.bind(BaseSettings, scope=SingletonScope, to=lambda: BaseSettings())

This does not

class Configuration(Module):
    def configure(self, binder: Binder):
        binder.bind(BaseSettings, scope=SingletonScope)

Reproduced issue

https://github.com/Guibod/pydantic-settings-bug-298

davidparsson commented 1 month ago

Thanks for reporting! Could you please post a stack trace or similar to aid anyone that would be willing to try to solve this?

Guibod commented 3 weeks ago

As displayed through pytest, while running the attached project with the reproducable error:

Python 3.8

E   TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'

raised by:

.tox/py38/lib/python3.8/site-packages/injector/__init__.py:91: in wrapper
    return function(*args, **kwargs)
.tox/py38/lib/python3.8/site-packages/injector/__init__.py:974: in get
    provider_instance = scope_instance.get(interface, binding.provider)
.tox/py38/lib/python3.8/site-packages/injector/__init__.py:91: in wrapper
    return function(*args, **kwargs)
.tox/py38/lib/python3.8/site-packages/injector/__init__.py:800: in get
    instance = self._get_instance(key, provider, self.injector)
.tox/py38/lib/python3.8/site-packages/injector/__init__.py:811: in _get_instance
    return provider.get(injector)
.tox/py38/lib/python3.8/site-packages/injector/__init__.py:264: in get
    return injector.create_object(self._cls)
.tox/py38/lib/python3.8/site-packages/injector/__init__.py:1002: in create_object
    reraise(e, CallError(instance, init_function, (), additional_kwargs, e, self._stack))
.tox/py38/lib/python3.8/site-packages/injector/__init__.py:190: in reraise
    raise exception.with_traceback(tb)
.tox/py38/lib/python3.8/site-packages/injector/__init__.py:998: in create_object
    self.call_with_injection(init, self_=instance, kwargs=additional_kwargs)
.tox/py38/lib/python3.8/site-packages/injector/__init__.py:1020: in call_with_injection
    bindings = get_bindings(callable)
.tox/py38/lib/python3.8/site-packages/injector/__init__.py:1161: in get_bindings
    type_hints = get_type_hints(callable, include_extras=True)
.tox/py38/lib/python3.8/site-packages/typing_extensions.py:1234: in get_type_hints
    hint = typing.get_type_hints(obj, globalns=globalns, localns=localns)
/opt/homebrew/opt/python@3.8/Frameworks/Python.framework/Versions/3.8/lib/python3.8/typing.py:1264: in get_type_hints
    value = _eval_type(value, globalns, localns)
/opt/homebrew/opt/python@3.8/Frameworks/Python.framework/Versions/3.8/lib/python3.8/typing.py:270: in _eval_type
    return t._evaluate(globalns, localns)
/opt/homebrew/opt/python@3.8/Frameworks/Python.framework/Versions/3.8/lib/python3.8/typing.py:518: in _evaluate
    eval(self.__forward_code__, globalns, localns),

Python 3.9

E   TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'

raised by

.tox/py39/lib/python3.9/site-packages/injector/__init__.py:91: in wrapper
    return function(*args, **kwargs)
.tox/py39/lib/python3.9/site-packages/injector/__init__.py:974: in get
    provider_instance = scope_instance.get(interface, binding.provider)
.tox/py39/lib/python3.9/site-packages/injector/__init__.py:91: in wrapper
    return function(*args, **kwargs)
.tox/py39/lib/python3.9/site-packages/injector/__init__.py:800: in get
    instance = self._get_instance(key, provider, self.injector)
.tox/py39/lib/python3.9/site-packages/injector/__init__.py:811: in _get_instance
    return provider.get(injector)
.tox/py39/lib/python3.9/site-packages/injector/__init__.py:264: in get
    return injector.create_object(self._cls)
.tox/py39/lib/python3.9/site-packages/injector/__init__.py:1002: in create_object
    reraise(e, CallError(instance, init_function, (), additional_kwargs, e, self._stack))
.tox/py39/lib/python3.9/site-packages/injector/__init__.py:190: in reraise
    raise exception.with_traceback(tb)
.tox/py39/lib/python3.9/site-packages/injector/__init__.py:998: in create_object
    self.call_with_injection(init, self_=instance, kwargs=additional_kwargs)
.tox/py39/lib/python3.9/site-packages/injector/__init__.py:1020: in call_with_injection
    bindings = get_bindings(callable)
.tox/py39/lib/python3.9/site-packages/injector/__init__.py:1161: in get_bindings
    type_hints = get_type_hints(callable, include_extras=True)
/opt/homebrew/Cellar/python@3.9/3.9.19/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py:1497: in get_type_hints
    value = _eval_type(value, globalns, localns)
/opt/homebrew/Cellar/python@3.9/3.9.19/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py:292: in _eval_type
    return t._evaluate(globalns, localns, recursive_guard)
/opt/homebrew/Cellar/python@3.9/3.9.19/Frameworks/Python.framework/Versions/3.9/lib/python3.9/typing.py:554: in _evaluate
    eval(self.__forward_code__, globalns, localns),
jstasiak commented 3 weeks ago

Hey @Guibod, sorry for your bad experience here.

From a quick look it looks like a pydantic-settings problem – using Python 3.10+ constructs on older Python versions – and I'm not sure trying to find a way to hack around that is the right thing to do.

Guibod commented 2 weeks ago

At least the issue is documented, as well as a work-around. So you don’t have to be sorry about it.

It was discarded by pydantic-settings team, so if you cannot achieve a solution on your own, i’m cool with it.

You can close or leave the ticket open. It’s your choice.