GithubHelp home page GithubHelp logo

reagento / dishka Goto Github PK

View Code? Open in Web Editor NEW
289.0 3.0 35.0 582 KB

Cute DI framework with agreeable API and everything you need

Home Page: https://dishka.readthedocs.io

License: Apache License 2.0

Python 99.44% Makefile 0.25% Batchfile 0.31%
di di-container di-framework ioc-container python dependency-injector dependency-injection

dishka's People

Contributors

andrewsergienko avatar daler-sz avatar dark04072006 avatar draincoder avatar ilya-nikolaev avatar ivankirpichnikov avatar lancetnik avatar lubaskinc0de avatar maxzhenzhera avatar olegt0rr avatar r-chulkevich avatar robz-tirtlib avatar shalmeo avatar skorpyon avatar sobolevn avatar suconakh avatar tishka17 avatar vlkorsakov avatar wrongnull avatar yarqr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

dishka's Issues

Per-request aliases

Allow to set alias when entering scope. That will allow to change logic for specific route without moving it to container

with container(aliases={A: A1}) as request_container:
   request_container.get(A) # this will reuturn A1

Aliases should be copied to inner scopes if we do not know the scope of depedency. But the do not affect the outer scope.

Graph validation

We need to ensure that all required dependencies can be actually created on startup.

  • all nodes of graph are reachable #85
  • all context varibles are passed
  • all requested types are reachable

Subgraphs

Use case:
Interactor depends on StatisticDbGateway and MainDbGateway. Each of them uses SQLAlchemy Session object, so they have similar init typehints. The difference is that StatisticDbGateway requires Session bound to Clickhouse Engine, while MainDbGateway binds to postgresql Engine. Again both engines have same type, but created differently.

To solve this you can replace Session with some new type (literally NewType("PgSession", Session)), but it requires modification of all code base. Imagine that you took those objects from external library and cannot modify it.

Another solution is again to use NewType, but create factories for all objects instead of registrering classes as depedencies directly. This requries a lot of work.

Proposed solution is to split those objects in 3 groups:

SubGraph1: StatisticDbGateway, Session, Engine
SubGraph2: MainDbGateway, Session, Engine
MainGraph: Interactor, SubGraph1, SubGraph2

Each graph here uses only his factories to solve dependencies or requests specially attached subgraphs. So here, gateways can request Session and they get exactly that session which is declared in his subgraph.

Requirements:

  1. (Sub)graphs can have subgraphs
  2. All (Sub)graphs are synchronized by scopes
  3. Subgraph cannot access depedencies from parent graph
  4. When looking for factories subgraphs are requested only if they are not found in current graph itself.

It is an open question whether (sub)graph is just a container or another thing

Resolve by parent class

class A: ...

class B(A): ...

p = Provider(scope=Scope.APP)
p.provide(B)

c = make_container(p)
c.get(A) # expected B()

Generics support

Let's image we have 3 classes:

class C(Generic[T]):
    pass

class Gw(Generic[T]):
    def __init__(self, c: C[T]): ...

class U:
    def __init__(self, Gw[Model]): ...

We should not declare factories for concrete intermediate classes like Gw in example. E.g. this is expected to work:

class MyProvider(Provider):
    @provide(scope=Scope.APP)
    def c_model(self) -> C[Model]: 
       return CModelImpl()

    gw = provide(Gw, scope=Scope.APP)
    u = provide(U, scope=Scope.APP)
...

container.get(U)

Service Repository Pattern Scopes

I'm moving from the python-dependency-injector library to this one and am using a service-repo pattern.
This is my small setup:

class DBProvider(Provider):
    @provide(scope=Scope.APP)
    def engine(self) -> AsyncEngine:
        engine = create_async_engine(
            get_config().ASYNC_DB_URL(),
            echo=True,
        )
        return engine


    @provide(scope=Scope.REQUEST)    
    async def session(self, engine: AsyncEngine) -> AsyncIterable[AsyncSession]:
        async with AsyncSession(engine) as session:
            yield session



class Interactor:
    def __init__(self, session: AsyncSession):
        self.session = session

    def test(self) -> str:
        return "test"
    

class InteractorProvider(Provider):
    i1 = provide(Interactor, scope=Scope.REQUEST)


class ServiceProvider(Provider):
    wallet_pass_api_service = provide(WalletPassApiService, scope=Scope.APP)
    block_user_repo = provide(BlockedUserRepository, scope=Scope.REQUEST)


container = make_async_container(
    DBProvider(), 
    ServiceProvider(),
    InteractorProvider()
)

When I use Scope.APP with the block_user_repo this does not work, as expected, since the BlockedUserRepositoryhas a dependency on theAsyncSessionwhich is aScope.REQUEST`.

But shouldn't all repositories and service be initialised on app startup? Because creating, possibly 10 or 20 repositories/services on each request result in large overhead?

Pass to annotation `Provider` instead of `FromComponent("component name")`

Motivation

It is inconvenient to use additional strings/constants for the sake of components.

class TelegramProvider(Provider):
    component = "Telegram"
    ...

    @provide(scope=Scope.REQUEST)
    async def get_user(self, telegram_object: TelegramObject) -> User:
        return telegram_object.from_user

@router.message()
async def handler(
    message: Message,
    user: Annotated[User, FromComponent("Telegram")],
) -> None: ...

Suggestion

It would be convenient to add MyProvider to the annotation, and then use its component field under the hood of the library.

@router.message()
async def handler(
    message: Message,
    user: Annotated[User, TelegramProvider],
) -> None: ...

Additional

This solution has one more advantage: we get the opportunity to move to the provider in 1 click (e.g. to edit it).
This is not possible in the current implementation (but it's possible in fastapi.Depends)

This solution also helps to avoid pyCharm bug with brackets highlighting: https://youtrack.jetbrains.com/issue/PY-57155/

Dishka dependency in FastAPI depends

How can I use injection on classes which are initialised on each route? The following code results in this error which is logical because I use FastAPI Depends. The error:

    raise fastapi.exceptions.FastAPIError(
fastapi.exceptions.FastAPIError: Invalid args for response field! Hint: check that <class 'common.services.permission_service.PermissionService'> is a valid Pydantic field type. If you are using a return type annotation that is not a valid Pydantic field (e.g. Union[Response, dict, None]) you can disable generating the response model from the type annotation with the path operation decorator parameter response_model=None. Read more: https://fastapi.tiangolo.com/tutorial/response-model/

My auth route:

@router.post('/token', response_model=BaseResponse[GetAuthTokenResponse])
@inject
async def token_authorize(request_data: AuthTokenRequest, user: AnnotatedAuthenticatedUser, auth_service: Annotated[AuthService, FromDishka()]):
    user, token = ....
    return schemas.BaseResponse[GetAuthTokenResponse](data={
        'user': user,
        'token': token.decode(),
    })

the AnnotatedAuthenticatedUser:

AnnotatedAuthenticatedUser = Annotated[AuthenticatedUser, Depends(AuthCheck())]

And the AuthCheck:

class AuthCheck:
    def __init__(self, permission: AuthCheckPermission = None):
        self._permission = permission

    def __call__(self,
                res: Response,
                req: Request,
                permission_service: Annotated[PermissionService, FromDishka()],
                invite_service: Annotated[InviteService, FromDishka()],
                event_service: Annotated[event_service.EventService, FromDishka()],
                organisation_service: Annotated[OrganizationService, FromDishka()],
                cred: HTTPAuthorizationCredentials=Depends(HTTPBearer(auto_error=False)),
    ):
        # with trace.get_tracer(__name__).start_as_current_span("auth_check") as _:
        if cred is None:
            raise InvalidAuthException
        try:
            decoded_token = auth.verify_id_token(cred.credentials)
        except Exception as err:
            raise InvalidAuthCredentialsException(original_exception=err)
...

Graph visualisation

Add function to render all dependencies into one file. HTML with Mermaid can be good for start

Documentation

  • Intro into dependency injection
  • Intro into IoC-containers
  • why we need scopes
  • what is providers
  • how to use container
  • async vs sync
  • decorator
  • alias
  • Integrations:
    • Flask
    • Fastapi
    • Litestar
    • aiogram
    • pytelegrambotapi
  • comparation with other libraries
    • rodi
    • di
    • dependency injector
    • fastapi depends

Incorrect ressolve for AnyOf

This code works correctly, but when I tried to support multiple interfaces for AsyncSession I got an exception

    @provide(scope=Scope.REQUEST)
    async def get_session(self, session_maker: async_sessionmaker[AsyncSession]) -> AsyncIterable[AsyncSession],
        async with session_maker() as session:
            yield session

This code already doesn't work

    @provide(scope=Scope.REQUEST)
    async def get_session(self, session_maker: async_sessionmaker[AsyncSession]) -> AnyOf[
        AsyncIterable[AsyncSession],
        AsyncIterable[interfaces.UoW]
    ]:
        async with session_maker() as session:
            yield session

Exception:

dependency_source/make_factory.py", line 269, in _make_factory_by_method
    raise TypeError(f"Failed to analyze `{name}`. \n" + str(e)) from e
TypeError: Failed to analyze `Container.get_session`. 
Unsupported return type `<dishka.entities.provides_marker.ProvideMultiple object at 0x7faea03bdb10>` for async generator. Did you mean AsyncIterable[<dishka.entities.provides_marker.ProvideMultiple object at 0x7faea03bdb10>]?

Skippable scopes

Sometimes we want to have additional itnermediate scopes in some run configurations. E.g

  1. In tests we want finalize app dependencies while keeping some objects in cache. In can be solved adding RUNTIME scope before APP
  2. For Websocket we have additional scope for connected client which doesn't exist for HTTP requests. Though, many object will be of the REQUEST scope.

The idea consists of two parts:

  1. An option to specificy scope when entering context. With this you will enter multiple scopes at one call. Exit will also rewind all entered scopes.
  2. An option on scope iteself to be skipped when entering wihout explicit scope.

So, if you enter context you
a) either provide scope explicitely, then all intermediate scopes will be also entered
b) do not provide any scope, then it is treated as the next non-skippable scopes. All scopes with mark skip will be enterd automatically

Add data when entering scope

E.g.

with container(request=value) as subcontainer:
   ...

request here should be available in solving context.
Or using typehint due to we don't have named dependencies:

with container({Request: value}) as subcontainer:
   ...

Graph validation fails with depends on Generic[T]

MRE

from abc import ABC

from dishka import make_container, Provider, Scope
from typing import Generic, TypeVar


class Event(ABC):
    ...


EventsT = TypeVar("EventsT")


class EventEmitter(Generic[EventsT], ABC):
    ...


class EventEmitterImpl(EventEmitter[EventsT]):
    ...


def factory(event_emitter: EventEmitter[Event]) -> int:
    return 1


provider = Provider()

provider.provide(EventEmitterImpl, scope=Scope.REQUEST, provides=EventEmitter)
provider.provide(factory, scope=Scope.REQUEST)

container = make_container(provider)

Traceback

Traceback (most recent call last):
  File "/home/lubaskincode/.config/JetBrains/PyCharm2023.3/scratches/test.py", line 32, in <module>
    container = make_container(provider)
  File "/home/lubaskincode/prj/PycharmProjects/zametka-api/.venv/lib/python3.10/site-packages/dishka/container.py", line 172, in make_container
    ).build()
  File "/home/lubaskincode/prj/PycharmProjects/zametka-api/.venv/lib/python3.10/site-packages/dishka/registry.py", line 340, in build
    GraphValidator(registries).validate()
  File "/home/lubaskincode/prj/PycharmProjects/zametka-api/.venv/lib/python3.10/site-packages/dishka/registry.py", line 161, in validate
    for factory in registry.factories.values():
RuntimeError: dictionary changed size during iteration

skip_validation=True as temporary fix

Add metadata of user-defined code to exceptions

As discussed on Podlodka Presentation, traceback have multiple Dishka internal calls between user incorrect code and traceback of exception, for example, dependency loop found. Quite useful to grab a metadata of user code, such as filename and position, and place that information close to ASCII image of dependency loop.

Performance benchmark

As discussed earlier, it would be great to publish a comparative analysis of the performance of Dishka against Depends from FastAPI, as well as against pure use, where the required dependency is obtained directly (without additional tools)

I believe that this will make it easier to decide whether to use this library or not

SqlAlchemy sessionmaker resolve failed

AsyncSessionMaker = async_sessionmaker

class DbProvider(Provider):
    def __init__(self, connection_string: str | AnyUrl, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.connection_string = str(connection_string)

    @provide(scope=Scope.APP)
    async def get_engine(self) -> AsyncEngine:
        dsn = self.connection_string.replace("postgresql", "postgresql+asyncpg")
        return create_async_engine(dsn)

    @provide(scope=Scope.APP)
    async def get_sessionmaker(self, engine: AsyncEngine) -> AsyncSessionMaker:
        return async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)

    @provide(scope=Scope.REQUEST)
    async def get_db_session(self, sessionmaker: AsyncSessionMaker) -> AsyncSession:
        return await get_or_create_db_session(sessionmaker)
dishka.exceptions.NoFactoryError: Cannot find factory for 
DependencyKey(type_hint=<class 'sqlalchemy.ext.asyncio.session.async_sessionmaker'>, component='')
requested by DependencyKey(type_hint=<class 'sqlalchemy.ext.asyncio.session.AsyncSession'>, component=''). 
It is missing or has invalid scope.```

Interface segregation syntaxes support

If my object supports few interfaces I need to write wrapper for each one.
Can you support some syntaxes for single object which support several interfaces?
For example:

@provide(scope=Scope.REQUEST) 
def get_db(self) -> I1 | I2 | I3:
    return object()

SQLAlchemy Async error

Multiple Asynchronous sqlalchemy calls are not working properly with the following code, synchronous does.
I read that I need to have an AsyncSession per asyncio task. How could I instruct Dishka to handle this?

class DBProvider(Provider):
    @provide(scope=Scope.APP)
    def engine(self) -> AsyncEngine:
        engine = create_async_engine(
            get_config().ASYNC_DB_URL(),
            echo=get_config().QUERY_ECHO,
            echo_pool=get_config().ECHO_POOL,
            pool_pre_ping=True,
            pool_size=get_config().DB_POOL_SIZE,
            json_serializer=custom_json_serialiser
        )
        return engine


    @provide(scope=Scope.REQUEST)
    async def session(self, engine: AsyncEngine) -> AsyncIterable[AsyncSession]:
        async with AsyncSession(engine, expire_on_commit=False) as session:
            yield session

Feed service:

class FeedService:
    def __init__(self, feed_item_repository: FeedItemRepository) -> None:
        self._feed_item_repository: FeedItemRepository = feed_item_repository


    async def get_organisation_feed(self, organisation_id: str, page: int = None, size: int = 20) -> List[FeedItem]:
        return await self._feed_item_repository.get_for_organisation(organisation_id, page, size, load_all=True)
    

    async def get_organisations_feed(self, organisation_ids: List[str], page: int = None, size: int = 20) -> List[FeedItem]:
        feed_items = []
        
        async def get_organisation_feed_task(organisation_id):
            organisation_feed = await self.get_organisation_feed(organisation_id)
            for feed_item in organisation_feed:
                feed_items.append(feed_item)

        tasks = []
        for organisation_id in organisation_ids:
            tasks.append(asyncio.create_task(get_organisation_feed_task(organisation_id)))

        await asyncio.gather(*tasks)

        return feed_items

Feed Item repo:

class FeedItemRepository(BaseRepository[FeedItem]):

    _model = FeedItem


    async def get_for_organisation(self, organisation_id: str, page: int = None, size: int = None, load_all: bool = False) -> list[FeedItem]:
        q = select(self._model).filter(self._model.organisation_id == organisation_id)

        if load_all:
            q = q.options(
                # joinedload(self._model.user),
                joinedload(self._model.organisation),
                joinedload(self._model.organisation),
                joinedload(self._model.event),
                joinedload(self._model.group),
                joinedload(self._model.message),
                joinedload(self._model.message_parent),
            )

        if page is not None and size is not None:
            q = q.limit(size).offset(page * size)

        q = q.order_by(self._model.created_at.desc())

        return (await self.session.execute(q)).scalars().unique().all()

When I call the FeedService.get_organisations_feed method it complains about this following:

Traceback (most recent call last):
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/dishka/integrations/starlette.py", line 41, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 62, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/routing.py", line 758, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/routing.py", line 778, in app
    await route.handle(scope, receive, send)
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/routing.py", line 299, in handle
    await self.app(scope, receive, send)
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/routing.py", line 79, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 64, in wrapped_app
    raise exc
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    await app(scope, receive, sender)
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/routing.py", line 74, in app
    response = await func(request)
               ^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/fastapi/routing.py", line 278, in app
    raw_response = await run_endpoint_function(
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/fastapi/routing.py", line 191, in run_endpoint_function
    return await dependant.call(**values)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/dishka/integrations/base.py", line 161, in autoinjected_func
    return await func(*args, **kwargs, **solved)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/api/routes/me.py", line 475, in get_me_feed
    feed_items = await feed_service.get_organisations_feed(user.organisation_ids_permissions, page=page)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/common/services/feed_service.py", line 32, in get_organisations_feed
    await asyncio.gather(*tasks)
  File "/Users/theobouwman/dev/projects/momo/momo-api/common/services/feed_service.py", line 24, in get_organisation_feed_task
    organisation_feed = await self.get_organisation_feed(organisation_id)
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/common/services/feed_service.py", line 17, in get_organisation_feed
    return await self._feed_item_repository.get_for_organisation(organisation_id, page, size, load_all=True)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/common/repositories/feed_item_repo.py", line 33, in get_for_organisation
    return (await self.session.execute(q)).scalars().unique().all()
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/session.py", line 452, in execute
    result = await greenlet_spawn(
             ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 186, in greenlet_spawn
    result = context.switch(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2306, in execute
    return self._execute_internal(
           ^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2181, in _execute_internal
    conn = self._connection_for_bind(bind)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2050, in _connection_for_bind
    return trans._connection_for_bind(engine, execution_options)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<string>", line 2, in _connection_for_bind
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/state_changes.py", line 103, in _go
    self._raise_for_prerequisite_state(fn.__name__, current_state)
  File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 945, in _raise_for_prerequisite_state
    raise sa_exc.InvalidRequestError(
sqlalchemy.exc.InvalidRequestError: This session is provisioning a new connection; concurrent operations are not permitted (Background on this error at: https://sqlalche.me/e/20/isce)

During handling of the above exception, another exception occurred:

  + Exception Group Traceback (most recent call last):
  |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/uvicorn/protocols/http/httptools_impl.py", line 419, in run_asgi
  |     result = await app(  # type: ignore[func-returns-value]
  |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
  |     return await self.app(scope, receive, send)
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/fastapi/applications.py", line 1054, in __call__
  |     await super().__call__(scope, receive, send)
  |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/applications.py", line 123, in __call__
  |     await self.middleware_stack(scope, receive, send)
  |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/middleware/errors.py", line 186, in __call__
  |     raise exc
  |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
  |     await self.app(scope, receive, _send)
  |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/middleware/cors.py", line 83, in __call__
  |     await self.app(scope, receive, send)
  |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/middleware/gzip.py", line 24, in __call__
  |     await responder(scope, receive, send)
  |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/starlette/middleware/gzip.py", line 44, in __call__
  |     await self.app(scope, receive, self.send_with_gzip)
  |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/dishka/integrations/starlette.py", line 37, in __call__
  |     async with request.app.state.dishka_container(
  | dishka.exceptions.ExitError: Cleanup context errors (1 sub-exception)
  +-+---------------- 1 ----------------
    | Traceback (most recent call last):
    |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/dishka/async_container.py", line 157, in close
    |     await anext(exit_generator.callable)
    |   File "/Users/theobouwman/dev/projects/momo/momo-api/di.py", line 79, in session
    |     async with AsyncSession(engine, expire_on_commit=False) as session:
    |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/session.py", line 1071, in __aexit__
    |     await asyncio.shield(task)
    |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/session.py", line 1016, in close
    |     await greenlet_spawn(self.sync_session.close)
    |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/util/_concurrency_py3k.py", line 186, in greenlet_spawn
    |     result = context.switch(*args, **kwargs)
    |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2462, in close
    |     self._close_impl(invalidate=False)
    |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/session.py", line 2531, in _close_impl
    |     transaction.close(invalidate)
    |   File "<string>", line 2, in close
    |   File "/Users/theobouwman/dev/projects/momo/momo-api/venv/lib/python3.11/site-packages/sqlalchemy/orm/state_changes.py", line 121, in _go
    |     raise sa_exc.IllegalStateChangeError(
    | sqlalchemy.exc.IllegalStateChangeError: Method 'close()' can't be called here; method '_connection_for_bind()' is already in progress and this would cause an unexpected state change to <SessionTransactionState.CLOSED: 5> (Background on this error at: https://sqlalche.me/e/20/isce)

Recursion in provide with source=random

MRO

from random import random

from dishka import provide, Provider, Scope


class MyProvider(Provider):
    scope = Scope.APP
    rand = provide(source=random, provides=float)

Supporte cycle dependencies

Add LazyProxy and put it instead of requested class when cycle is detected.

It should be enabled explicitly for each cycle

more readable errors

if I've forgot scope i received this error

E               KeyError: None

..\..\AppData\Local\pypoetry\Cache\virtualenvs\shvatka-7O9jFSMq-py3.11\Lib\site-packages\dishka\registry.py:84: KeyError

please add to error provider name (and will be greate if method name or provided type for case with really many @provide functiuons in one provider)

if I created sync container and add async provider i receive next error:

ValueError: Unsupported type FactoryType.ASYNC_GENERATOR

will be greate to add to error why (wrong type of container)

Dependency alias

Sometimes (especially when we follow interface segregation principle) we have single implementation for multiple dependencies. It would be useful to have single instance for all of them.

Let's
Current way to make alias:

class MyProvider(Provider):
    @provide(scope=MyScope.REQUEST)
    def get_repo(self, repo: UserRepositoryImpl) -> UserGetter:
       return repo

We can rely on the fact that scope must be the same, so this can be simplified to:

class MyProvider(Provider):
    repo = alias(UserRepositoryImpl, dependency=UserGetter)

Per-provider scopes

it is exhausting to declare scope for each depedency as most of the will have Request scope. It woul be easier to set it on Provder level. E.g

class MyProvider(Provider):
    scope = Scope.REQUEST

    a = provide(A)   # will have Scope.REQUEST
    b = provide(B, scope=Scope.APP)   # will have Scope.APP

Decorate providers

Sometimes we have a dependency provided by library and we want to modify it (like decorating).
So, the next provider will receive some dependency (+additional), and provide the same type of dependency

Pass exception to generators

When we finalize dependencies we have no information if there were exception during process handling.

We can use send or asend to pass it without breaking compatibility.

Also we will need to add an optional parameter to close method if Container object and modify it's __exit__

Support multiple decorators on method

@provide(provides=A)
@provide(provides=AProto)
def foo(self, a: A) -> A:
    return A()


@decorate(provides=A)
@decorate(provides=AProto)
def bar(self, a: A) -> A:
    return A()

Disabling lazy loading in provide

Idea:
I suggest adding the ability to disable lazy dependency loading for the 'provide' function or globally for the entire 'Provider'

How is it supposed to work?
When creating a new container, create all dependencies with the 'lazy=False' flag and add them to the cache.

Why do you need it?
Create resource-intensive sessions or pools, as well as detect errors early when creating dependencies and protect against errors during code execution

How I see it in the code
Provide

class MyProvider(Provider)
    a = provide(A, scope=Scope.App, lazy=False)

Provider

class MyProvider(Provider)
    lazy = Fasle
    a = provide(A, scope=Scope.App)

How to mock dependencies in tests

Hey!

Is it possible override dependecies like it realized in dependency-injector?

with container.api_client_factory.override(unittest.mock.Mock(ApiClient)):
    service2 = container.service_factory()
    assert isinstance(service2.api_client, unittest.mock.Mock)

SqlAlchemy AsyncSession resolve failed

Hi! I created DbProvider:

AsyncSessionMaker = async_sessionmaker[AsyncSession]

_db_session_ctx: ContextVar[AsyncSession | None] = ContextVar("_db_session_ctx", default=None)


class ContextSessionMaker(AsyncSessionMaker):
    def __call__(self, **local_kw) -> AsyncSession:
        session = _db_session_ctx.get()
        if session is None:
            session = super().__call__(**local_kw)
            _db_session_ctx.set(session)
        return session


class DbProvider(Provider):
    def __init__(self, conn_string: str, *args, **kwargs) -> None:
        super().__init__(*args, **kwargs)
        self.conn_string = conn_string

    @provide(scope=Scope.APP)
    async def get_engine(self) -> AsyncEngine:
        return create_async_engine(self.conn_string)

    @provide(scope=Scope.APP)
    async def get_sessionmaker(self, engine: AsyncEngine) -> ContextSessionMaker:
        return ContextSessionMaker(engine, class_=AsyncSession, expire_on_commit=False)

    @provide(scope=Scope.REQUEST)
    async def get_db_session(
        self, sessionmaker: ContextSessionMaker
    ) -> AsyncIterable[AsyncSession]:
        async with sessionmaker() as db_session:
            yield db_session

But in tests i got error:
dishka.exceptions.NoFactoryError: Cannot find factory for (<class 'sqlalchemy.ext.asyncio.session.AsyncSession'>, component=''). Check scopes in your providers. It is missing or has invalid scope.

@pytest.fixture()
async def container() -> AsyncIterator[dishka.AsyncContainer]:
    db_provider = DbProvider("sqlite+aiosqlite:///:memory:")
    container = dishka.make_async_container(db_provider)
    yield container
    await container.close()

@pytest.fixture()
async def db_session(container: dishka.AsyncContainer) -> AsyncSession:
    return await container.get(AsyncSession)

dishka version: 1.1.1

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.