lagom
lagom copied to clipboard
Cannot get _is_coroutine on injectable when mock
Error when inject async function:
/cache/asdf/installs/python/3.8.11/lib/python3.8/unittest/mock.py:1322:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/cache/asdf/installs/python/3.8.11/lib/python3.8/unittest/mock.py:1304: in decoration_helper
arg = exit_stack.enter_context(patching)
/cache/asdf/installs/python/3.8.11/lib/python3.8/unittest/mock.py:1477: in __enter__
new = create_autospec(autospec, spec_set=spec_set,
/cache/asdf/installs/python/3.8.11/lib/python3.8/unittest/mock.py:2607: in create_autospec
mock = Klass(parent=_parent, _new_parent=_parent, _new_name=_new_name,
/cache/asdf/installs/python/3.8.11/lib/python3.8/unittest/mock.py:2006: in __init__
_safe_super(MagicMixin, self).__init__(*args, **kw)
/cache/asdf/installs/python/3.8.11/lib/python3.8/unittest/mock.py:444: in __init__
self._mock_add_spec(spec, spec_set, _spec_as_instance, _eat_self)
/cache/asdf/installs/python/3.8.11/lib/python3.8/unittest/mock.py:499: in _mock_add_spec
if asyncio.iscoroutinefunction(getattr(spec, attr, None)):
/cache/asdf/installs/python/3.8.11/lib/python3.8/asyncio/coroutines.py:167: in iscoroutinefunction
getattr(func, '_is_coroutine', None) is _is_coroutine)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <lagom.markers.Injectable object at 0xffffb8796610>, item = '_is_coroutine'
def __getattr__(self, item: str):
"""
injectable should never have it's attributes referenced or a method call.
This normally indicates that the default injectable value hasn't been
handled by lagom - which is likely a function missing a bind decorator.
"""
# Ignore dunder methods as it's likely some decorator magic and
# it doesn't really help to raise an exception then.
if item.startswith("__") and item.endswith("__"):
return None
> raise InjectableNotResolved(
f"Cannot get {item} on injectable. Make sure the function was bound to a container instance"
)
E lagom.exceptions.InjectableNotResolved: Cannot get _is_coroutine on injectable. Make sure the function was bound to a container instance
.venv/lib/python3.8/site-packages/lagom/markers.py:46: InjectableNotResolved
@rodfersou interesting. Have you got some sample code I could use to replicate this?
it's ~~really breaking:~~
import asyncio
from abc import ABC
from lagom import bind_to_container, Container, dependency_definition, injectable
from lagom.environment import Env
## Assume we have a Thing with a slightly different class in dev
## or maybe just in a different deployment
class Thing(ABC):
async def run(self):
...
class ProdVersionOfThing(Thing):
def __init__(self, config):
self.config = config
def run(self):
print(f"PROD: {self.config}")
class DevVersionOfThing(Thing):
def run(self):
print("DEV THING")
## Lagom provides an env class to represent a logical
## grouping of environment variables
class ThingEnvironment(Env):
# This maps to an environment variable THING_CONN
thing_conn: str
## We define a dependency_definition function for
## our Thing which fetches the env from the container
## automatically loading the env variable.
## Note: if the env variable is unset an error will be raised
container = Container()
@dependency_definition(container, singleton=True)
def _thing_loader(c: Container) -> Thing:
connection_string = c[ThingEnvironment].thing_conn
if connection_string.startswith("dev://"):
return DevVersionOfThing()
return ProdVersionOfThing(connection_string)
@bind_to_container(container)
async def main(thing: Thing = injectable):
thing.run()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
still working.. investigating if ~the problem is with pytest
~
looking at the traceback, the problem is related with unittest.mock
and async
in a function with injectable
default value
Ah this makes more sense then. I've not done much mocking of any entities that are decorated and have injectable arguments. It's not really a usecase I'd considered so it possibly has some unexpected behaviour like this. I'll need to spend some time investigating to give a clearer answer.
understand, in our case we need to rely on mock simply because we are testing the API, and some calls happen before reach the entities that are injectable.
One of the design goals of lagom was to avoid having mocking at this level so I'd be interested to see what your test cases look like. If you've got an example you could share that would be really helpful.
in this specific test we are treating the system as a blackbox
, so we do a GET
or POST
in one endpoint and get the results
by default we have some testing / production versions of the dependencies, and this work great
but for example we want to force the system to behave like the user don't have permission to do the action, so we want to change the dependency to one that raises an exception
Agree that would be possible to do this change directly in the container, but turned out that if I use this pattern:
@bind_to_container(container)
def main(thing: Thing = injectable):
thing.run()
the container can't be overriten, it's like the implementation of the decorator is not allowing to access changes outside the function scope, that's why I need to be creative to fix using a mock:
from functools import partial
from unittest import mock
import my_module
@mock.patch(
"my_module.my_injected_function",
partial(my_module.my_injected_function, injected_parameter="Custom Value"),
)
def test_thing() -> None:
response = get_json(
f"/my/api/{some_test_id}"
)
assert ...
Thanks. The way I would test a function like:
@bind_to_container(container)
def main(thing: Thing = injectable):
thing.run()
would be
from unittest import mock
from my_module import main
def test_thing() -> None:
mock_thing = mock(Thing)
result = main(mock_thing)
assert ...
Ideally with lagom functions bound to the container should become "pure" with respect to their input so you would not need to mock the function itself.
But in any case the injected marker and container decorator should behave better when mocked so I'll look into this a little more.
Interestingly my straight-forward mocking of a bound function seems to work: 3bb77e241082d52c1c60e6669564f8336135ffe7 so I'm struggling to replicate this at the moment.
@meadsteve like I described, it's not an unit test, it's an integration test, in this case you don't have direct access to the function that you are injecting dependency