Scopes

Scopes are used to help further manage the lifetime of dependencies in your applications.

Note

Scopes are useful for systems like webservers or worker queues, where certain objects have different lifetime rules. For simple command line applications, these concepts can be ignored.

When setting bindings, there is an option to set the scope as either application or request scoped. By default, dependencies are set to request scoped.

  • Application Scope:
    Dependencies are created once per application and reused for all requests.
  • Request Scope:
    Dependencies are created newly for each request that is made.
    Within a single request, objects will still be reused.

When resolving objects with the graph, both scopes will always exist. Scopes are constructed for you if not manually created. For example, the following:

async def main():
    graph = FlexGraph()

    res = await graph.resolve(create_foo)

Is equivalent to:

async def main():
    graph = FlexGraph()

    async with graph.application_scope() as app_scope:
        async with app_scope.request_scope() as req_scope:
            res = await req_scope.resolve(create_foo)

Example Setup

We’ll start by making some basic classes that we can bind to the graph in various ways:

class Foo:
    pass

class Bar:
    pass

With providers:

def create_foo() -> Iterator[Foo]:
    print("Starting Foo")
    yield Foo()
    print("Ending Foo")

def create_bar() -> Iterator[Bar]:
    print("Starting Bar")
    yield Bar()
    print("Ending Bar")

Using the graph directly

We can use the FlexGraph directly without the entrypoint directive. The graph supports invoking any callable which has dependencies registered by the graph. When the graph is called directly, it does not persist any objects between invocations.

async def multiple_resolves() -> None:
    graph = FlexGraph()

    print("Example Start")

    foo1 = await graph.resolve(create_foo)
    foo2 = await graph.resolve(create_foo)
    print("Foo1 is Foo2:", foo1 is foo2)

    print("Example End")
Example Start
Starting Foo
Ending Foo
Starting Foo
Ending Foo
Foo1 is Foo2: False
Example End

We can see that create_foo is called twice, and that foo is shut down as soon as we get the result back. This is because each scope is newly created and destroyed to processes each request.

If we want to re-use results throughout multiple calls, we need to use scopes!

Request Scope

In our previous example, if we want create_foo to be called only once, then we could do the following:

async def request_scoped_resolve() -> None:
    graph = FlexGraph()

    print("Before App Scope")
    async with graph.application_scope() as app_scope:
        print("In App Scope")

        print("Before Req Scope")
        async with app_scope.request_scope() as req_scope:
            print("In Req Scope")

            foo1 = await req_scope.resolve(create_foo)
            foo2 = await req_scope.resolve(create_foo)
            print("Foo1 is Foo2:", foo1 is foo2)

        print("After Req Scope")
    print("After App Scope")
Before App Scope
In App Scope
Before Req Scope
In Req Scope
Starting Foo
Foo1 is Foo2: True
Ending Foo
After Req Scope
After App Scope

Because we re-used the request scope for multiple calls, we re-used the instance. You’ll also notice that the shutdown of the resource happens only after we close the request scope.

Application Scope

We could also choose to make create_foo set as application scoped:

async def application_scoped_resolve() -> None:
    graph = FlexGraph()

    graph.bind(create_foo, scope="application")

    print("Before App Scope")
    async with graph.application_scope() as app_scope:
        print("In App Scope")

        foo1 = await app_scope.resolve(create_foo)
        foo2 = await app_scope.resolve(create_foo)
        print("Foo1 is Foo2:", foo1 is foo2)

    print("After App Scope")
Before App Scope
In App Scope
Starting Foo
Foo1 is Foo2: True
Ending Foo
After App Scope

Similar to last time, because the application scope was re-used, we re-used the instance. However, this time the shutdown of the resource happens only after we close the application scope.

Eager Dependencies

By default, dependencies are created lazily when requested for the first time. If you want a value to be created as soon as the scope is opened, then you can set the dependency as eager.

The following example illustrates how eager dependencies interact with the different scopes:

async def eager_dependencies() -> None:
    graph = FlexGraph()

    graph.bind(create_foo, scope="application", eager=True)
    graph.bind(create_bar, scope="request", eager=True)

    print("Before App Scope")
    async with graph.application_scope() as app_scope:
        print("In App Scope")

        print("Before Req Scope")
        async with app_scope.request_scope() as req_scope:
            print("In Req Scope")
        print("After Req Scope")

    print("After App Scope")
Before App Scope
Starting Foo
In App Scope
Before Req Scope
Starting Bar
In Req Scope
Ending Bar
After Req Scope
Ending Foo
After App Scope

Even though we didn’t specifically call create_foo or create_bar, they were opened when entering the scope they were associated with. We also notice that they are closed only after their associated scope has also been closed.